@axpecter/lync 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +95 -143
  2. package/package.json +1 -1
  3. package/src/Types.luau +22 -16
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +95 -72
  6. package/src/api/Query.luau +128 -79
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +21 -14
  10. package/src/codec/composite/Array.luau +56 -134
  11. package/src/codec/composite/Map.luau +103 -72
  12. package/src/codec/composite/Optional.luau +6 -2
  13. package/src/codec/composite/Shared.luau +160 -69
  14. package/src/codec/composite/Struct.luau +283 -42
  15. package/src/codec/composite/Tagged.luau +13 -16
  16. package/src/codec/composite/Tuple.luau +21 -15
  17. package/src/codec/datatype/Buffer.luau +6 -4
  18. package/src/codec/datatype/CFrame.luau +56 -44
  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/Enum.luau +9 -9
  34. package/src/codec/meta/Float.luau +6 -28
  35. package/src/codec/meta/Nothing.luau +1 -1
  36. package/src/codec/meta/Unknown.luau +10 -7
  37. package/src/codec/primitive/Bool.luau +6 -4
  38. package/src/codec/primitive/Float16.luau +5 -2
  39. package/src/codec/primitive/Int.luau +5 -6
  40. package/src/codec/primitive/Number.luau +6 -4
  41. package/src/codec/primitive/Signed.luau +2 -2
  42. package/src/codec/primitive/Varint.luau +69 -38
  43. package/src/index.d.ts +161 -53
  44. package/src/init.luau +123 -102
  45. package/src/internal/Baseline.luau +9 -1
  46. package/src/internal/Channel.luau +153 -154
  47. package/src/internal/Middleware.luau +22 -6
  48. package/src/internal/Pool.luau +12 -4
  49. package/src/internal/Registry.luau +25 -16
  50. package/src/internal/Transport.luau +1 -1
  51. package/src/transport/Bridge.luau +47 -33
  52. package/src/transport/Client.luau +25 -21
  53. package/src/transport/Gate.luau +227 -172
  54. package/src/transport/Reader.luau +162 -105
  55. package/src/transport/Server.luau +46 -57
  56. package/src/util/Array.luau +18 -0
  57. package/src/util/Buffer.luau +92 -0
  58. package/src/util/Constants.luau +30 -0
  59. package/src/util/Log.luau +68 -0
  60. package/src/util/Player.luau +14 -0
  61. package/src/util/Quantize.luau +60 -0
  62. package/src/internal/Util.luau +0 -26
@@ -2,6 +2,10 @@
2
2
  --!native
3
3
  -- Per-channel buffer state with MSB batch framing, XOR delta, timestamps.
4
4
 
5
+ local Base = require(script.Parent.Parent.codec.Base)
6
+ local Buf = require(script.Parent.Parent.util.Buffer)
7
+ local Constants = require(script.Parent.Parent.util.Constants)
8
+ local Log = require(script.Parent.Parent.util.Log)
5
9
  local Types = require(script.Parent.Parent.Types)
6
10
  local Varint = require(script.Parent.Parent.codec.primitive.Varint)
7
11
 
@@ -12,16 +16,29 @@ local TS_FRAME = 1
12
16
  local TS_OFFSET = 2
13
17
  local TS_FULL = 3
14
18
 
15
- local DEFAULT_MAX = 262144
16
19
  local INITIAL_BUF = 1024
17
- local FRAME_MASK = 0xFF
18
- local OFFSET_WIN = 65.536
20
+ local HEADER_BYTE = 1
21
+ local FRAME_MSB = Constants.FRAME_MSB
22
+ local U8_MAX = Constants.U8_MAX
23
+ local U16_MAX = Constants.U16_MAX
24
+ local MAX_BATCH_ITEMS = Constants.MAX_BATCH_ITEMS
25
+
26
+ -- 65.535s window so round((clock % win) * 1000) cannot overflow u16.
27
+ local OFFSET_WIN = 65.535
28
+
29
+ local TS_BYTES = table.freeze({
30
+ [TS_NONE] = 0,
31
+ [TS_FRAME] = 1,
32
+ [TS_OFFSET] = 2,
33
+ [TS_FULL] = 8,
34
+ })
19
35
 
20
36
  -- State ------------------------------------------------------------------
21
37
 
22
- local _maxSize = DEFAULT_MAX
23
- local _anyTimestamp = false
38
+ local _timestampsEnabled = false
24
39
  local _statsEnabled = false
40
+
41
+ -- Refreshed by updateTimestamp once per flush.
25
42
  local _frameCounter = 0
26
43
  local _offsetMs = 0
27
44
  local _clock = 0.0
@@ -30,50 +47,20 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
30
47
 
31
48
  -- Private ----------------------------------------------------------------
32
49
 
33
- local countlz = bit32.countlz
34
- local lshift = bit32.lshift
50
+ local alloc = Base.alloc
35
51
  local band = bit32.band
36
- local bxor = bit32.bxor
37
52
  local bor = bit32.bor
53
+ local bufCopy = buffer.copy
38
54
  local writeu8 = buffer.writeu8
39
55
  local writeu16 = buffer.writeu16
40
- local writeu32 = buffer.writeu32
41
56
  local writef64 = buffer.writef64
42
- local readu8 = buffer.readu8
43
- local readu32 = buffer.readu32
44
57
  local round = math.round
45
- local minN = math.min
58
+ local osClock = os.clock
46
59
 
47
- local function alloc(ch: Types.ChannelState, bytes: number): ()
48
- local needed = ch.cursor + bytes
49
- if needed <= ch.size then
50
- return
51
- end
52
- if needed > _maxSize then
53
- local name = ch.currentPacket
54
- if name then
55
- error(`[Lync] Channel.alloc("{name}"): buffer overflow ({needed}B, max {_maxSize}B)`)
56
- end
57
- error(`[Lync] Channel.alloc: buffer overflow ({needed}B, max {_maxSize}B)`)
60
+ local function ensure(ch: Types.ChannelState, n: number): ()
61
+ if ch.cursor + n > ch.size then
62
+ alloc(ch, n)
58
63
  end
59
- local newSize = lshift(1, 32 - countlz(needed - 1))
60
- local newBuff = buffer.create(newSize)
61
- buffer.copy(newBuff, 0, ch.buff, 0, ch.cursor)
62
- ch.buff = newBuff
63
- ch.size = newSize
64
- end
65
-
66
- local function tsBytes(mode: number): number
67
- if mode == TS_NONE then
68
- return 0
69
- end
70
- if mode == TS_FRAME then
71
- return 1
72
- end
73
- if mode == TS_OFFSET then
74
- return 2
75
- end
76
- return 8
77
64
  end
78
65
 
79
66
  local function writeTimestamp(b: buffer, off: number, mode: number): ()
@@ -86,8 +73,66 @@ local function writeTimestamp(b: buffer, off: number, mode: number): ()
86
73
  end
87
74
  end
88
75
 
76
+ --[[
77
+ Single -> multi upgrade. Patches the previously-emitted single-item
78
+ header, splices a 2-byte count cell after the timestamp, and shifts the
79
+ payload right. Hot only when a packet fires twice in a row to the same
80
+ channel without an intervening flush.
81
+ ]]
82
+ local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, headerSize: number): ()
83
+ local headerPos = ch.singlePos
84
+ writeu8(ch.buff, headerPos, regId)
85
+
86
+ local afterTs = headerPos + headerSize
87
+ local cursor = ch.cursor
88
+ local payloadLen = cursor - afterTs
89
+ ensure(ch, 2)
90
+ -- Re-bind: ensure() may have realloced the buffer.
91
+ local b = ch.buff
92
+
93
+ if payloadLen > 0 then
94
+ bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
95
+ end
96
+
97
+ ch.countPos = afterTs
98
+ writeu16(b, afterTs, 0)
99
+ ch.cursor = cursor + 2
100
+ ch.singleMode = false
101
+ end
102
+
103
+ --[[
104
+ Two writeBatch implementations, swapped at startup by enableStats(). For
105
+ high-rate channels this saves ~1 register read + 1 compare + 1 jump per
106
+ item versus branching on `_statsEnabled` inside the loop.
107
+ ]]
108
+ local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
109
+ local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
110
+
111
+ writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
112
+ if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
113
+ reg._openFn(ch, reg.id)
114
+ elseif ch.singleMode then
115
+ upgradeSingleToMulti(ch, reg.id, reg.headerSize)
116
+ end
117
+ ch.itemCount += 1
118
+ reg.codec.write(ch, data)
119
+ end
120
+
121
+ writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
122
+ if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
123
+ reg._openFn(ch, reg.id)
124
+ elseif ch.singleMode then
125
+ upgradeSingleToMulti(ch, reg.id, reg.headerSize)
126
+ end
127
+ ch.itemCount += 1
128
+ local before = ch.cursor
129
+ reg.codec.write(ch, data)
130
+ reg.bytesSent += ch.cursor - before
131
+ end
132
+
89
133
  -- Public -----------------------------------------------------------------
90
134
 
135
+ -- Module table is intentionally unfrozen: enableStats() hot-swaps writeBatch.
91
136
  local Channel = {}
92
137
 
93
138
  Channel.TS_NONE = TS_NONE
@@ -114,36 +159,19 @@ function Channel.create(): Types.ChannelState
114
159
  }
115
160
  end
116
161
 
117
- function Channel.alloc(ch: Types.ChannelState, bytes: number): ()
118
- if bytes > 0 then
119
- alloc(ch, bytes)
120
- end
121
- end
122
-
123
- --[[
124
- Write a single byte at the cursor and advance. Centralizes the
125
- ubiquitous `if cursor + 1 > size then alloc(ch, 1) end; writeu8(...);
126
- cursor += 1` pattern.
127
- ]]
128
162
  function Channel.writeByte(ch: Types.ChannelState, byte: number): ()
129
163
  local cursor = ch.cursor
130
- if cursor + 1 > ch.size then
131
- alloc(ch, 1)
164
+ if cursor + HEADER_BYTE > ch.size then
165
+ alloc(ch, HEADER_BYTE)
132
166
  end
133
167
  writeu8(ch.buff, cursor, byte)
134
- ch.cursor = cursor + 1
168
+ ch.cursor = cursor + HEADER_BYTE
135
169
  end
136
170
 
137
- --[[
138
- Push `value` into the channel's sidecar refs array, write its
139
- 1-based index as a u16 to the buffer, advance cursor. Returns the
140
- index. Used by the Instance and Unknown codecs.
141
- ]]
142
- function Channel.pushRef(ch: Types.ChannelState, value: any): number
171
+ -- Append `value` to the sidecar refs, write its 1-based u16 index.
172
+ function Channel.pushRef(ch: Types.ChannelState, value: any): ()
143
173
  local idx = ch.refCount + 1
144
- if idx > 0xFFFF then
145
- error("[Lync] Channel.pushRef: ref overflow (>65535)")
146
- end
174
+ Log.assert(idx <= U16_MAX, "ref overflow, max 65535")
147
175
  ch.refs[idx] = value
148
176
  ch.refCount = idx
149
177
 
@@ -153,9 +181,12 @@ function Channel.pushRef(ch: Types.ChannelState, value: any): number
153
181
  end
154
182
  writeu16(ch.buff, cursor, idx)
155
183
  ch.cursor = cursor + 2
156
- return idx
157
184
  end
158
185
 
186
+ --[[
187
+ Per-flush reset. Preserves prevDump/prevDumpLen (XOR baseline) and
188
+ ch.deltas (delta caches): both must outlive flushes for the same owner.
189
+ ]]
159
190
  function Channel.reset(ch: Types.ChannelState): ()
160
191
  ch.cursor = 0
161
192
  ch.lastId = -1
@@ -170,20 +201,34 @@ function Channel.reset(ch: Types.ChannelState): ()
170
201
  end
171
202
  end
172
203
 
204
+ --[[
205
+ Full reset including XOR baseline and delta caches. Pool.acquire calls
206
+ this so a recycled state cannot leak the previous owner's baseline.
207
+ ]]
208
+ function Channel.fullReset(ch: Types.ChannelState): ()
209
+ Channel.reset(ch)
210
+ ch.prevDump = nil
211
+ ch.prevDumpLen = 0
212
+ table.clear(ch.deltas)
213
+ end
214
+
173
215
  --[[
174
216
  Resolve the batch-open closure for a given timestamp mode and packet
175
- name. The returned function:
176
- - seals the previous batch (writing the deferred multi-mode count),
177
- - writes the single-mode header [0x80 | id] + optional timestamp,
178
- - records cursor position so writeBatch can upgrade single->multi later.
217
+ name. Returned function seals the prior batch, writes the single-mode
218
+ header [0x80 | id] + optional timestamp, and records the cursor so
219
+ writeBatch can upgrade single -> multi later.
179
220
  ]]
180
221
  function Channel.resolveOpenFn(
181
222
  timestampMode: number,
182
223
  name: string
183
224
  ): (ch: Types.ChannelState, id: number) -> ()
184
- local headerLen = 1 + tsBytes(timestampMode)
225
+ local headerLen = HEADER_BYTE + TS_BYTES[timestampMode]
185
226
 
186
227
  return function(ch: Types.ChannelState, id: number): ()
228
+ --[[
229
+ Inline sealCount: this is the only call site that can take the
230
+ multi-mode branch on a non-fresh channel.
231
+ ]]
187
232
  if ch.lastId >= 0 and not ch.singleMode then
188
233
  writeu16(ch.buff, ch.countPos, ch.itemCount)
189
234
  end
@@ -197,7 +242,7 @@ function Channel.resolveOpenFn(
197
242
  alloc(ch, headerLen)
198
243
  end
199
244
  local b = ch.buff
200
- writeu8(b, cursor, bor(0x80, id))
245
+ writeu8(b, cursor, bor(FRAME_MSB, id))
201
246
  writeTimestamp(b, cursor + 1, timestampMode)
202
247
 
203
248
  ch.singleMode = true
@@ -206,95 +251,31 @@ function Channel.resolveOpenFn(
206
251
  end
207
252
  end
208
253
 
254
+ Channel.writeBatch = writeBatchPlain
255
+
209
256
  --[[
210
- Write one item into the current batch. Handles single->multi mode
211
- upgrade when a second item lands on the same packet ID: clears the
212
- header byte's MSB, then memmoves payload right by 2 to insert the
213
- deferred u16 count between the timestamp and first payload.
257
+ Every caller (Server.flushReliable / flushUnreliable, Client.flush)
258
+ pre-checks `ch.cursor > 0`, so cursor here is always positive.
214
259
  ]]
215
- function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
216
- if reg.id ~= ch.lastId or ch.itemCount >= 0xFFFF then
217
- reg._openFn(ch, reg.id)
218
- elseif ch.singleMode then
219
- local headerPos = ch.singlePos
220
- local b = ch.buff
221
- writeu8(b, headerPos, reg.id)
222
-
223
- local afterTs = headerPos + reg.headerSize
224
- local cursor = ch.cursor
225
- local payloadLen = cursor - afterTs
226
- if cursor + 2 > ch.size then
227
- alloc(ch, 2)
228
- end
229
- local b2 = ch.buff
230
-
231
- if payloadLen > 0 then
232
- buffer.copy(b2, afterTs + 2, b2, afterTs, payloadLen)
233
- end
234
-
235
- ch.countPos = afterTs
236
- writeu16(b2, afterTs, 0)
237
- ch.cursor = cursor + 2
238
- ch.singleMode = false
239
- end
240
-
241
- ch.itemCount += 1
242
- reg.codec.write(ch, data)
243
- end
244
-
245
260
  function Channel.sealAndDump(ch: Types.ChannelState): (buffer, { Instance }, number, number)
246
- if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
261
+ if ch.lastId >= 0 and not ch.singleMode then
247
262
  writeu16(ch.buff, ch.countPos, ch.itemCount)
248
263
  end
249
264
 
250
265
  local cursor = ch.cursor
251
266
  local snapshot = buffer.create(cursor)
252
- if cursor > 0 then
253
- buffer.copy(snapshot, 0, ch.buff, 0, cursor)
254
- end
267
+ bufCopy(snapshot, 0, ch.buff, 0, cursor)
255
268
 
256
269
  local refCount = ch.refCount
257
270
  local refs = if refCount > 0 then table.clone(ch.refs) else EMPTY_REFS
258
271
  return snapshot, refs, cursor, refCount
259
272
  end
260
273
 
261
- --[[
262
- XOR `current` against `previous`, returning a fresh buffer of curLen
263
- bytes. When `previous` is nil, returns `current` unchanged.
264
- ]]
265
- function Channel.xorApply(
266
- current: buffer,
267
- curLen: number,
268
- previous: buffer?,
269
- prevLen: number
270
- ): buffer
271
- if not previous then
272
- return current
273
- end
274
-
275
- local result = buffer.create(curLen)
276
- local overlap = minN(curLen, prevLen)
277
- local prev = previous
278
-
279
- local aligned = band(overlap, -4)
280
- local i = 0
281
- while i < aligned do
282
- writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
283
- i += 4
284
- end
285
- while i < overlap do
286
- writeu8(result, i, bxor(readu8(current, i), readu8(prev, i)))
287
- i += 1
288
- end
289
- if curLen > overlap then
290
- buffer.copy(result, overlap, current, overlap, curLen - overlap)
291
- end
292
- return result
293
- end
274
+ Channel.xorApply = Buf.xorApply
294
275
 
295
276
  --[[
296
- Write a query frame. MSB on the id byte signals nil response;
297
- cleared means a payload follows. Correlation IDs are varint-encoded.
277
+ Query frame. MSB on the id byte = nil response; cleared = payload follows.
278
+ Correlation IDs are varint-encoded.
298
279
  ]]
299
280
  function Channel.writeQuery(
300
281
  ch: Types.ChannelState,
@@ -303,14 +284,19 @@ function Channel.writeQuery(
303
284
  codec: Types.InternalCodec<any>?,
304
285
  data: any
305
286
  ): ()
306
- if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
287
+ if ch.lastId >= 0 and not ch.singleMode then
307
288
  writeu16(ch.buff, ch.countPos, ch.itemCount)
308
289
  end
290
+ --[[
291
+ lastId = -1 forces the next writeBatch to call _openFn (which resets
292
+ singleMode). Every other reader of singleMode (writeQuery,
293
+ sealAndDump) is gated by `lastId >= 0` first, so stamping singleMode
294
+ here would never be observed before the next reset overwrites it.
295
+ ]]
309
296
  ch.lastId = -1
310
- ch.singleMode = false
311
297
 
312
298
  local hasPayload = codec ~= nil and data ~= nil
313
- local headerByte = if hasPayload then id else bor(0x80, id)
299
+ local headerByte = if hasPayload then id else bor(FRAME_MSB, id)
314
300
 
315
301
  local cursor = ch.cursor
316
302
  if cursor + 1 > ch.size then
@@ -328,29 +314,42 @@ end
328
314
  -- Configuration ----------------------------------------------------------
329
315
 
330
316
  function Channel.setMaxSize(bytes: number): ()
331
- _maxSize = bytes
317
+ Base.setMaxSize(bytes)
332
318
  end
333
319
 
334
320
  function Channel.updateTimestamp(): ()
335
- _frameCounter = band(_frameCounter + 1, FRAME_MASK)
336
- _offsetMs = round((os.clock() % OFFSET_WIN) * 1000)
337
- _clock = os.clock()
321
+ _frameCounter = band(_frameCounter + 1, U8_MAX)
322
+ local now = osClock()
323
+ _offsetMs = round((now % OFFSET_WIN) * 1000)
324
+ _clock = now
338
325
  end
339
326
 
340
327
  function Channel.enableTimestamps(): ()
341
- _anyTimestamp = true
328
+ _timestampsEnabled = true
342
329
  end
343
330
 
344
331
  function Channel.hasTimestamps(): boolean
345
- return _anyTimestamp
332
+ return _timestampsEnabled
346
333
  end
347
334
 
348
335
  function Channel.enableStats(): ()
349
336
  _statsEnabled = true
337
+ Channel.writeBatch = writeBatchStats
350
338
  end
351
339
 
352
340
  function Channel.statsEnabled(): boolean
353
341
  return _statsEnabled
354
342
  end
355
343
 
356
- return table.freeze(Channel)
344
+ -- Module-level config + timestamp counters. Distinct from Channel.reset(ch).
345
+ function Channel.resetGlobals(): ()
346
+ Base.reset()
347
+ _timestampsEnabled = false
348
+ _statsEnabled = false
349
+ _frameCounter = 0
350
+ _offsetMs = 0
351
+ _clock = 0.0
352
+ Channel.writeBatch = writeBatchPlain
353
+ end
354
+
355
+ return Channel
@@ -13,13 +13,21 @@ local _dropSignal = Signal.create()
13
13
 
14
14
  -- Private ----------------------------------------------------------------
15
15
 
16
+ -- Mirrors Signal's STATE_DISCONNECTED. Entries in this state are mid-fire disconnects.
17
+ local STATE_DISCONNECTED = 0
18
+
16
19
  --[[
17
- Walk a transform chain in order. Each callback returns a transformed
18
- value or nil to keep the previous value. A fresh snapshot guards
19
- against disconnect-during-fire reentry.
20
+ Snapshot before iterating: a handler can disconnect itself or others,
21
+ and a fresh chain pull would skip or double-fire entries depending on
22
+ swap direction. Snapshotting locks the per-frame view.
20
23
  ]]
21
- local function runChain(signal: any, value: any, name: string, player: Player?): any
22
- local count = signal._count[1]
24
+ local function runChain(
25
+ signal: { _count: number, _entries: { any } },
26
+ value: any,
27
+ name: string,
28
+ player: Player?
29
+ ): any
30
+ local count = signal._count
23
31
  if count == 0 then
24
32
  return value
25
33
  end
@@ -31,8 +39,9 @@ local function runChain(signal: any, value: any, name: string, player: Player?):
31
39
  local result = value
32
40
  for i = 1, count do
33
41
  local entry = snapshot[i]
34
- if entry._state ~= 0 then
42
+ if entry._state ~= STATE_DISCONNECTED then
35
43
  local returned = entry.fn(result, name, player)
44
+ -- Hooks return non-nil to replace the value; nil keeps the prior result.
36
45
  if returned ~= nil then
37
46
  result = returned
38
47
  end
@@ -73,6 +82,7 @@ function Middleware.fireDrop(player: Player, reason: string, name: string, data:
73
82
  _dropSignal:fireSync(player, reason, name, data)
74
83
  end
75
84
 
85
+ -- Hot-path callers gate on these to skip runChain's snapshot allocation.
76
86
  function Middleware.hasSendHooks(): boolean
77
87
  return _sendSignal:hasListeners()
78
88
  end
@@ -81,4 +91,10 @@ function Middleware.hasReceiveHooks(): boolean
81
91
  return _receiveSignal:hasListeners()
82
92
  end
83
93
 
94
+ function Middleware.reset(): ()
95
+ _sendSignal = Signal.create()
96
+ _receiveSignal = Signal.create()
97
+ _dropSignal = Signal.create()
98
+ end
99
+
84
100
  return table.freeze(Middleware)
@@ -1,9 +1,9 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Stack-based ChannelState pool. Reset-on-acquire keeps released
4
- -- channels reusable without per-release reset cost.
3
+ -- ChannelState pool. Acquire fullResets to wipe prior owner state.
5
4
 
6
5
  local Channel = require(script.Parent.Channel)
6
+ local Log = require(script.Parent.Parent.util.Log)
7
7
  local Types = require(script.Parent.Parent.Types)
8
8
 
9
9
  -- Constants --------------------------------------------------------------
@@ -20,17 +20,19 @@ local _maxSize = DEFAULT_SIZE
20
20
 
21
21
  local Pool = {}
22
22
 
23
+ -- fullReset wipes the prior owner's XOR baseline and delta caches.
23
24
  function Pool.acquire(): Types.ChannelState
24
25
  if _depth > 0 then
25
26
  local ch = _stack[_depth]
26
27
  _stack[_depth] = nil
27
28
  _depth -= 1
28
- Channel.reset(ch)
29
+ Channel.fullReset(ch)
29
30
  return ch
30
31
  end
31
32
  return Channel.create()
32
33
  end
33
34
 
35
+ -- Above the cap, drop on the floor; the GC reclaims the buffer.
34
36
  function Pool.release(ch: Types.ChannelState): ()
35
37
  if _depth >= _maxSize then
36
38
  return
@@ -41,7 +43,7 @@ end
41
43
 
42
44
  function Pool.setMaxSize(size: number): ()
43
45
  if size < 0 then
44
- error(`[Lync] Pool.setMaxSize: size must be >= 0, got {size}`)
46
+ Log.error(`size must be non-negative, got {size}`)
45
47
  end
46
48
  _maxSize = size
47
49
  while _depth > _maxSize do
@@ -54,4 +56,10 @@ function Pool.count(): number
54
56
  return _depth
55
57
  end
56
58
 
59
+ function Pool.reset(): ()
60
+ table.clear(_stack)
61
+ _depth = 0
62
+ _maxSize = DEFAULT_SIZE
63
+ end
64
+
57
65
  return table.freeze(Pool)
@@ -1,7 +1,8 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Deterministic packet/query ID assignment. Sequential 7-bit IDs.
3
+ -- Sequential 7-bit packet/query ID assignment.
4
4
 
5
+ local Log = require(script.Parent.Parent.util.Log)
5
6
  local Signal = require(script.Parent.Parent.api.Signal)
6
7
  local Types = require(script.Parent.Parent.Types)
7
8
 
@@ -10,13 +11,15 @@ local Types = require(script.Parent.Parent.Types)
10
11
  local KIND_PACKET = 0
11
12
  local KIND_REQUEST = 1
12
13
  local KIND_RESPONSE = 2
14
+
15
+ -- 7-bit id leaves the MSB free for the frame single/multi flag (see Constants.FRAME_MSB).
13
16
  local MAX_ID = 127
14
17
 
15
18
  -- State ------------------------------------------------------------------
16
19
 
17
20
  local _nextId = 0
18
- local _byId: { [number]: Types.Registration } = {}
19
- local _byName: { [string]: Types.Registration } = {}
21
+ local _registrationsById: { [number]: Types.Registration } = {}
22
+ local _registrationsByName: { [string]: Types.Registration } = {}
20
23
 
21
24
  -- Public -----------------------------------------------------------------
22
25
 
@@ -45,10 +48,14 @@ export type RegisterOptions = {
45
48
  function Registry.register(opts: RegisterOptions): Types.Registration
46
49
  local nextCandidate = _nextId + 1
47
50
  if nextCandidate > MAX_ID then
48
- error(`[Lync] Registry.register: ID limit exceeded ({MAX_ID})`)
51
+ Log.error(`ID limit ({MAX_ID}) exceeded; cannot register "{opts.name}"`)
49
52
  end
50
- if opts.kind ~= KIND_RESPONSE and _byName[opts.name] then
51
- error(`[Lync] Registry.register: duplicate name "{opts.name}"`)
53
+ --[[
54
+ RESPONSE entries share their request's name with a "\0resp" suffix; only
55
+ request/packet names need uniqueness in the by-name index.
56
+ ]]
57
+ if opts.kind ~= KIND_RESPONSE and _registrationsByName[opts.name] ~= nil then
58
+ Log.error(`duplicate name "{opts.name}"`)
52
59
  end
53
60
 
54
61
  _nextId = nextCandidate
@@ -75,36 +82,38 @@ function Registry.register(opts: RegisterOptions): Types.Registration
75
82
  drops = 0,
76
83
  }
77
84
 
78
- _byId[nextCandidate] = reg
85
+ _registrationsById[nextCandidate] = reg
79
86
  if opts.kind ~= KIND_RESPONSE then
80
- _byName[opts.name] = reg
87
+ _registrationsByName[opts.name] = reg
81
88
  end
82
89
  return reg
83
90
  end
84
91
 
85
92
  function Registry.get(id: number): Types.Registration?
86
- return _byId[id]
93
+ return _registrationsById[id]
87
94
  end
88
95
 
89
96
  function Registry.getByName(name: string): Types.Registration?
90
- return _byName[name]
97
+ return _registrationsByName[name]
91
98
  end
92
99
 
93
100
  function Registry.count(): number
94
101
  return _nextId
95
102
  end
96
103
 
104
+ -- IDs are sequential and never freed, so the table has no holes.
97
105
  function Registry.all(): { Types.Registration }
98
106
  local result = table.create(_nextId)
99
- local n = 0
100
107
  for i = 1, _nextId do
101
- local reg = _byId[i]
102
- if reg then
103
- n += 1
104
- result[n] = reg
105
- end
108
+ result[i] = _registrationsById[i]
106
109
  end
107
110
  return result
108
111
  end
109
112
 
113
+ function Registry.reset(): ()
114
+ table.clear(_registrationsById)
115
+ table.clear(_registrationsByName)
116
+ _nextId = 0
117
+ end
118
+
110
119
  return table.freeze(Registry)
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Lazy-require Server/Client to break the api -> transport import cycle.
3
+ -- Lazy Server/Client require to break the api -> transport import cycle.
4
4
 
5
5
  -- State ------------------------------------------------------------------
6
6