@axpecter/lync 2.1.1 → 2.2.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 (56) hide show
  1. package/README.md +241 -349
  2. package/package.json +1 -1
  3. package/src/Types.luau +53 -13
  4. package/src/api/Group.luau +41 -51
  5. package/src/api/Packet.luau +145 -143
  6. package/src/api/Query.luau +204 -202
  7. package/src/api/Scope.luau +36 -38
  8. package/src/api/Signal.luau +68 -94
  9. package/src/codec/Base.luau +43 -23
  10. package/src/codec/composite/Array.luau +161 -152
  11. package/src/codec/composite/Map.luau +37 -53
  12. package/src/codec/composite/Optional.luau +15 -21
  13. package/src/codec/composite/Shared.luau +78 -41
  14. package/src/codec/composite/Struct.luau +64 -125
  15. package/src/codec/composite/Tagged.luau +15 -25
  16. package/src/codec/composite/Tuple.luau +15 -20
  17. package/src/codec/datatype/Buffer.luau +44 -51
  18. package/src/codec/datatype/CFrame.luau +72 -104
  19. package/src/codec/datatype/Color.luau +14 -10
  20. package/src/codec/datatype/Instance.luau +32 -42
  21. package/src/codec/datatype/IntVector.luau +24 -22
  22. package/src/codec/datatype/NumberRange.luau +21 -12
  23. package/src/codec/datatype/Ray.luau +13 -7
  24. package/src/codec/datatype/Rect.luau +13 -7
  25. package/src/codec/datatype/Region.luau +25 -22
  26. package/src/codec/datatype/Sequence.luau +144 -118
  27. package/src/codec/datatype/String.luau +29 -59
  28. package/src/codec/datatype/UDim.luau +29 -30
  29. package/src/codec/datatype/Vector.luau +89 -105
  30. package/src/codec/meta/Auto.luau +194 -246
  31. package/src/codec/meta/Bitfield.luau +37 -36
  32. package/src/codec/meta/Custom.luau +10 -10
  33. package/src/codec/meta/Enum.luau +8 -11
  34. package/src/codec/meta/Float.luau +16 -20
  35. package/src/codec/meta/Nothing.luau +2 -2
  36. package/src/codec/meta/Unknown.luau +22 -32
  37. package/src/codec/primitive/Bool.luau +9 -5
  38. package/src/codec/primitive/Float16.luau +13 -23
  39. package/src/codec/primitive/Int.luau +20 -28
  40. package/src/codec/primitive/Number.luau +6 -10
  41. package/src/codec/primitive/Signed.luau +32 -0
  42. package/src/codec/primitive/Varint.luau +59 -28
  43. package/src/index.d.ts +9 -3
  44. package/src/init.luau +118 -143
  45. package/src/internal/Baseline.luau +17 -18
  46. package/src/internal/Channel.luau +143 -212
  47. package/src/internal/Middleware.luau +37 -47
  48. package/src/internal/Pool.luau +10 -10
  49. package/src/internal/Registry.luau +38 -34
  50. package/src/internal/Transport.luau +28 -0
  51. package/src/internal/Util.luau +26 -0
  52. package/src/transport/Bridge.luau +32 -24
  53. package/src/transport/Client.luau +51 -53
  54. package/src/transport/Gate.luau +183 -113
  55. package/src/transport/Reader.luau +95 -66
  56. package/src/transport/Server.luau +130 -125
@@ -1,10 +1,11 @@
1
1
  --!strict
2
2
  --!native
3
- -- Per-channel buffer state with MSB batch framing, XOR, timestamps.
3
+ -- Per-channel buffer state with MSB batch framing, XOR delta, timestamps.
4
4
 
5
5
  local Types = require(script.Parent.Parent.Types)
6
+ local Varint = require(script.Parent.Parent.codec.primitive.Varint)
6
7
 
7
- -- Constants -----------------------------------------------------------
8
+ -- Constants --------------------------------------------------------------
8
9
 
9
10
  local TS_NONE = 0
10
11
  local TS_FRAME = 1
@@ -12,9 +13,11 @@ local TS_OFFSET = 2
12
13
  local TS_FULL = 3
13
14
 
14
15
  local DEFAULT_MAX = 262144
15
- local EMPTY_REFS = table.freeze({})
16
+ local INITIAL_BUF = 1024
17
+ local FRAME_MASK = 0xFF
18
+ local OFFSET_WIN = 65.536
16
19
 
17
- -- State ---------------------------------------------------------------
20
+ -- State ------------------------------------------------------------------
18
21
 
19
22
  local _maxSize = DEFAULT_MAX
20
23
  local _anyTimestamp = false
@@ -23,20 +26,23 @@ local _frameCounter = 0
23
26
  local _offsetMs = 0
24
27
  local _clock = 0.0
25
28
 
26
- -- Private -------------------------------------------------------------
29
+ local EMPTY_REFS = table.freeze({}) :: { Instance }
30
+
31
+ -- Private ----------------------------------------------------------------
27
32
 
28
33
  local countlz = bit32.countlz
29
34
  local lshift = bit32.lshift
35
+ local band = bit32.band
30
36
  local bxor = bit32.bxor
31
37
  local bor = bit32.bor
32
- local band = bit32.band
33
38
  local writeu8 = buffer.writeu8
34
39
  local writeu16 = buffer.writeu16
40
+ local writeu32 = buffer.writeu32
35
41
  local writef64 = buffer.writef64
36
42
  local readu8 = buffer.readu8
37
43
  local readu32 = buffer.readu32
38
44
  local round = math.round
39
- local min = math.min
45
+ local minN = math.min
40
46
 
41
47
  local function alloc(ch: Types.ChannelState, bytes: number): ()
42
48
  local needed = ch.cursor + bytes
@@ -44,11 +50,11 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
44
50
  return
45
51
  end
46
52
  if needed > _maxSize then
47
- error(
48
- if ch.currentPacket
49
- then `[Lync] Channel: buffer overflow ({needed}B, max {_maxSize}B) in "{ch.currentPacket}"`
50
- else `[Lync] Channel: buffer overflow ({needed}B, max {_maxSize}B)`
51
- )
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)`)
52
58
  end
53
59
  local newSize = lshift(1, 32 - countlz(needed - 1))
54
60
  local newBuff = buffer.create(newSize)
@@ -57,7 +63,30 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
57
63
  ch.size = newSize
58
64
  end
59
65
 
60
- -- Public --------------------------------------------------------------
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
+ end
78
+
79
+ local function writeTimestamp(b: buffer, off: number, mode: number): ()
80
+ if mode == TS_FRAME then
81
+ writeu8(b, off, _frameCounter)
82
+ elseif mode == TS_OFFSET then
83
+ writeu16(b, off, _offsetMs)
84
+ elseif mode == TS_FULL then
85
+ writef64(b, off, _clock)
86
+ end
87
+ end
88
+
89
+ -- Public -----------------------------------------------------------------
61
90
 
62
91
  local Channel = {}
63
92
 
@@ -68,9 +97,9 @@ Channel.TS_FULL = TS_FULL
68
97
 
69
98
  function Channel.create(): Types.ChannelState
70
99
  return {
71
- buff = buffer.create(1024),
100
+ buff = buffer.create(INITIAL_BUF),
72
101
  cursor = 0,
73
- size = 1024,
102
+ size = INITIAL_BUF,
74
103
  refs = {},
75
104
  refCount = 0,
76
105
  lastId = -1,
@@ -86,7 +115,45 @@ function Channel.create(): Types.ChannelState
86
115
  end
87
116
 
88
117
  function Channel.alloc(ch: Types.ChannelState, bytes: number): ()
89
- alloc(ch, bytes)
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
+ function Channel.writeByte(ch: Types.ChannelState, byte: number): ()
129
+ local cursor = ch.cursor
130
+ if cursor + 1 > ch.size then
131
+ alloc(ch, 1)
132
+ end
133
+ writeu8(ch.buff, cursor, byte)
134
+ ch.cursor = cursor + 1
135
+ end
136
+
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
143
+ local idx = ch.refCount + 1
144
+ if idx > 0xFFFF then
145
+ error("[Lync] Channel.pushRef: ref overflow (>65535)")
146
+ end
147
+ ch.refs[idx] = value
148
+ ch.refCount = idx
149
+
150
+ local cursor = ch.cursor
151
+ if cursor + 2 > ch.size then
152
+ alloc(ch, 2)
153
+ end
154
+ writeu16(ch.buff, cursor, idx)
155
+ ch.cursor = cursor + 2
156
+ return idx
90
157
  end
91
158
 
92
159
  function Channel.reset(ch: Types.ChannelState): ()
@@ -104,121 +171,56 @@ function Channel.reset(ch: Types.ChannelState): ()
104
171
  end
105
172
 
106
173
  --[[
107
- Resolves the batch-open function for a given timestamp mode.
108
- The returned function handles MSB single-item framing:
109
- First item: writes [0x80 | id] (single mode, no count).
110
- Second item: upgrades to multi mode with u16 count.
174
+ 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.
111
179
  ]]
112
180
  function Channel.resolveOpenFn(
113
181
  timestampMode: number,
114
182
  name: string
115
183
  ): (ch: Types.ChannelState, id: number) -> ()
116
- if timestampMode == TS_NONE then
117
- return function(ch: Types.ChannelState, id: number): ()
118
- -- Seal previous batch
119
- if ch.lastId >= 0 and not ch.singleMode then
120
- writeu16(ch.buff, ch.countPos, ch.itemCount)
121
- end
122
- ch.currentPacket = name
123
- ch.lastId = id
124
- ch.itemCount = 0
125
-
126
- -- Write single-mode header: [1IIIIIII]
127
- local cursor = ch.cursor
128
- if cursor + 1 > ch.size then
129
- alloc(ch, 1)
130
- end
131
- writeu8(ch.buff, cursor, bor(0x80, id))
132
- ch.singleMode = true
133
- ch.singlePos = cursor
134
- ch.cursor = cursor + 1
135
- end
136
- elseif timestampMode == TS_FRAME then
137
- return function(ch: Types.ChannelState, id: number): ()
138
- if ch.lastId >= 0 and not ch.singleMode then
139
- writeu16(ch.buff, ch.countPos, ch.itemCount)
140
- end
141
- ch.currentPacket = name
142
- ch.lastId = id
143
- ch.itemCount = 0
144
-
145
- local cursor = ch.cursor
146
- if cursor + 2 > ch.size then
147
- alloc(ch, 2)
148
- end
149
- local b = ch.buff
150
- writeu8(b, cursor, bor(0x80, id))
151
- writeu8(b, cursor + 1, _frameCounter)
152
- ch.singleMode = true
153
- ch.singlePos = cursor
154
- ch.cursor = cursor + 2
155
- end
156
- elseif timestampMode == TS_OFFSET then
157
- return function(ch: Types.ChannelState, id: number): ()
158
- if ch.lastId >= 0 and not ch.singleMode then
159
- writeu16(ch.buff, ch.countPos, ch.itemCount)
160
- end
161
- ch.currentPacket = name
162
- ch.lastId = id
163
- ch.itemCount = 0
164
-
165
- local cursor = ch.cursor
166
- if cursor + 3 > ch.size then
167
- alloc(ch, 3)
168
- end
169
- local b = ch.buff
170
- writeu8(b, cursor, bor(0x80, id))
171
- writeu16(b, cursor + 1, _offsetMs)
172
- ch.singleMode = true
173
- ch.singlePos = cursor
174
- ch.cursor = cursor + 3
184
+ local headerLen = 1 + tsBytes(timestampMode)
185
+
186
+ return function(ch: Types.ChannelState, id: number): ()
187
+ if ch.lastId >= 0 and not ch.singleMode then
188
+ writeu16(ch.buff, ch.countPos, ch.itemCount)
175
189
  end
176
- else -- TS_FULL
177
- return function(ch: Types.ChannelState, id: number): ()
178
- if ch.lastId >= 0 and not ch.singleMode then
179
- writeu16(ch.buff, ch.countPos, ch.itemCount)
180
- end
181
- ch.currentPacket = name
182
- ch.lastId = id
183
- ch.itemCount = 0
184
-
185
- local cursor = ch.cursor
186
- if cursor + 9 > ch.size then
187
- alloc(ch, 9)
188
- end
189
- local b = ch.buff
190
- writeu8(b, cursor, bor(0x80, id))
191
- writef64(b, cursor + 1, _clock)
192
- ch.singleMode = true
193
- ch.singlePos = cursor
194
- ch.cursor = cursor + 9
190
+
191
+ ch.currentPacket = name
192
+ ch.lastId = id
193
+ ch.itemCount = 0
194
+
195
+ local cursor = ch.cursor
196
+ if cursor + headerLen > ch.size then
197
+ alloc(ch, headerLen)
195
198
  end
199
+ local b = ch.buff
200
+ writeu8(b, cursor, bor(0x80, id))
201
+ writeTimestamp(b, cursor + 1, timestampMode)
202
+
203
+ ch.singleMode = true
204
+ ch.singlePos = cursor
205
+ ch.cursor = cursor + headerLen
196
206
  end
197
207
  end
198
208
 
199
209
  --[[
200
- Writes one item into a batch. Handles MSB singlemulti upgrade.
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.
201
214
  ]]
202
215
  function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
203
216
  if reg.id ~= ch.lastId or ch.itemCount >= 0xFFFF then
204
217
  reg._openFn(ch, reg.id)
205
218
  elseif ch.singleMode then
206
- --[[
207
- Second item for same packet: upgrade to multi mode.
208
- Rewrite header byte to clear MSB, insert u16 count after.
209
- ]]
210
- local b = ch.buff
211
219
  local headerPos = ch.singlePos
212
- writeu8(b, headerPos, reg.id) -- MSB cleared = multi-item mode
213
-
214
- local tsBytes = reg.headerSize - 1
215
- local afterTs = headerPos + 1 + tsBytes
220
+ local b = ch.buff
221
+ writeu8(b, headerPos, reg.id)
216
222
 
217
- --[[
218
- Upgrading from single→multi requires inserting 2 bytes (u16 count)
219
- between the timestamp and the first item's payload, shifting
220
- existing data right by 2.
221
- ]]
223
+ local afterTs = headerPos + reg.headerSize
222
224
  local cursor = ch.cursor
223
225
  local payloadLen = cursor - afterTs
224
226
  if cursor + 2 > ch.size then
@@ -231,7 +233,7 @@ function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, dat
231
233
  end
232
234
 
233
235
  ch.countPos = afterTs
234
- writeu16(b2, afterTs, 0) -- placeholder u16; sealAndDump writes final count
236
+ writeu16(b2, afterTs, 0)
235
237
  ch.cursor = cursor + 2
236
238
  ch.singleMode = false
237
239
  end
@@ -240,32 +242,25 @@ function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, dat
240
242
  reg.codec.write(ch, data)
241
243
  end
242
244
 
243
- --[[
244
- Seals the current batch and returns a snapshot for transmission.
245
- ]]
246
245
  function Channel.sealAndDump(ch: Types.ChannelState): (buffer, { Instance }, number, number)
247
- -- Multi-item batches need their count finalized before transmission
248
246
  if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
249
247
  writeu16(ch.buff, ch.countPos, ch.itemCount)
250
248
  end
251
249
 
252
250
  local cursor = ch.cursor
253
251
  local snapshot = buffer.create(cursor)
254
- buffer.copy(snapshot, 0, ch.buff, 0, cursor)
255
-
256
- local refs: { Instance }
257
- local refCount = ch.refCount
258
- if refCount > 0 then
259
- refs = table.clone(ch.refs)
260
- else
261
- refs = EMPTY_REFS :: any
252
+ if cursor > 0 then
253
+ buffer.copy(snapshot, 0, ch.buff, 0, cursor)
262
254
  end
263
255
 
256
+ local refCount = ch.refCount
257
+ local refs = if refCount > 0 then table.clone(ch.refs) else EMPTY_REFS
264
258
  return snapshot, refs, cursor, refCount
265
259
  end
266
260
 
267
261
  --[[
268
- XOR current frame against previous for deflate-friendly output.
262
+ XOR `current` against `previous`, returning a fresh buffer of curLen
263
+ bytes. When `previous` is nil, returns `current` unchanged.
269
264
  ]]
270
265
  function Channel.xorApply(
271
266
  current: buffer,
@@ -278,34 +273,28 @@ function Channel.xorApply(
278
273
  end
279
274
 
280
275
  local result = buffer.create(curLen)
281
- local overlap = min(curLen, prevLen)
282
- local prev = previous :: buffer
276
+ local overlap = minN(curLen, prevLen)
277
+ local prev = previous
283
278
 
284
- -- u32 chunks
285
- local aligned = overlap - (overlap % 4)
279
+ local aligned = band(overlap, -4)
286
280
  local i = 0
287
281
  while i < aligned do
288
- buffer.writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
282
+ writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
289
283
  i += 4
290
284
  end
291
-
292
- -- u8 remainder of overlap
293
285
  while i < overlap do
294
286
  writeu8(result, i, bxor(readu8(current, i), readu8(prev, i)))
295
287
  i += 1
296
288
  end
297
-
298
- -- Excess bytes (current longer than previous)
299
289
  if curLen > overlap then
300
290
  buffer.copy(result, overlap, current, overlap, curLen - overlap)
301
291
  end
302
-
303
292
  return result
304
293
  end
305
294
 
306
295
  --[[
307
- Writes a query frame with MSB encoding for nil-status.
308
- MSB set = nil response, MSB clear = payload follows.
296
+ Write a query frame. MSB on the id byte signals nil response;
297
+ cleared means a payload follows. Correlation IDs are varint-encoded.
309
298
  ]]
310
299
  function Channel.writeQuery(
311
300
  ch: Types.ChannelState,
@@ -314,95 +303,37 @@ function Channel.writeQuery(
314
303
  codec: Types.InternalCodec<any>?,
315
304
  data: any
316
305
  ): ()
317
- -- Queries are standalone frames, so close any open packet batch first
318
306
  if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
319
307
  writeu16(ch.buff, ch.countPos, ch.itemCount)
320
308
  end
321
309
  ch.lastId = -1
322
310
  ch.singleMode = false
323
311
 
324
- if codec and data ~= nil then
325
- local cursor = ch.cursor
326
- if cursor + 1 > ch.size then
327
- alloc(ch, 1)
328
- end
329
- writeu8(ch.buff, cursor, id)
330
- ch.cursor = cursor + 1
331
-
332
- -- Inlined varint avoids the function call overhead for correlation IDs
333
- if correlationId <= 0xBF then
334
- local c2 = ch.cursor
335
- if c2 + 1 > ch.size then
336
- alloc(ch, 1)
337
- end
338
- writeu8(ch.buff, c2, correlationId)
339
- ch.cursor = c2 + 1
340
- elseif correlationId <= 8383 then
341
- local c2 = ch.cursor
342
- if c2 + 2 > ch.size then
343
- alloc(ch, 2)
344
- end
345
- local adjusted = correlationId - 192
346
- writeu8(ch.buff, c2, bor(0xC0, band(adjusted // 256, 0x1F)))
347
- writeu8(ch.buff, c2 + 1, band(adjusted, 0xFF))
348
- ch.cursor = c2 + 2
349
- else
350
- local c2 = ch.cursor
351
- if c2 + 5 > ch.size then
352
- alloc(ch, 5)
353
- end
354
- writeu8(ch.buff, c2, 0xF0)
355
- buffer.writeu32(ch.buff, c2 + 1, correlationId)
356
- ch.cursor = c2 + 5
357
- end
312
+ local hasPayload = codec ~= nil and data ~= nil
313
+ local headerByte = if hasPayload then id else bor(0x80, id)
358
314
 
359
- codec.write(ch, data)
360
- else
361
- -- MSB set signals nil response to the reader
362
- local cursor = ch.cursor
363
- if cursor + 1 > ch.size then
364
- alloc(ch, 1)
365
- end
366
- writeu8(ch.buff, cursor, bor(0x80, id))
367
- ch.cursor = cursor + 1
368
-
369
- if correlationId <= 0xBF then
370
- local c2 = ch.cursor
371
- if c2 + 1 > ch.size then
372
- alloc(ch, 1)
373
- end
374
- writeu8(ch.buff, c2, correlationId)
375
- ch.cursor = c2 + 1
376
- elseif correlationId <= 8383 then
377
- local c2 = ch.cursor
378
- if c2 + 2 > ch.size then
379
- alloc(ch, 2)
380
- end
381
- local adjusted = correlationId - 192
382
- writeu8(ch.buff, c2, bor(0xC0, band(adjusted // 256, 0x1F)))
383
- writeu8(ch.buff, c2 + 1, band(adjusted, 0xFF))
384
- ch.cursor = c2 + 2
385
- else
386
- local c2 = ch.cursor
387
- if c2 + 5 > ch.size then
388
- alloc(ch, 5)
389
- end
390
- writeu8(ch.buff, c2, 0xF0)
391
- buffer.writeu32(ch.buff, c2 + 1, correlationId)
392
- ch.cursor = c2 + 5
393
- end
315
+ local cursor = ch.cursor
316
+ if cursor + 1 > ch.size then
317
+ alloc(ch, 1)
318
+ end
319
+ writeu8(ch.buff, cursor, headerByte)
320
+ ch.cursor = cursor + 1
321
+
322
+ Varint.write(ch, correlationId)
323
+ if hasPayload then
324
+ (codec :: Types.InternalCodec<any>).write(ch, data)
394
325
  end
395
326
  end
396
327
 
397
- -- Configuration -------------------------------------------------------
328
+ -- Configuration ----------------------------------------------------------
398
329
 
399
330
  function Channel.setMaxSize(bytes: number): ()
400
331
  _maxSize = bytes
401
332
  end
402
333
 
403
334
  function Channel.updateTimestamp(): ()
404
- _frameCounter = (_frameCounter + 1) % 256
405
- _offsetMs = round((os.clock() % 65.536) * 1000)
335
+ _frameCounter = band(_frameCounter + 1, FRAME_MASK)
336
+ _offsetMs = round((os.clock() % OFFSET_WIN) * 1000)
406
337
  _clock = os.clock()
407
338
  end
408
339
 
@@ -1,17 +1,47 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Ordered intercept chains returning Connection objects.
3
+ -- Ordered intercept chains for send/receive/drop hooks.
4
4
 
5
5
  local Signal = require(script.Parent.Parent.api.Signal)
6
6
  local Types = require(script.Parent.Parent.Types)
7
7
 
8
- -- State ---------------------------------------------------------------
8
+ -- State ------------------------------------------------------------------
9
9
 
10
10
  local _sendSignal = Signal.create()
11
11
  local _receiveSignal = Signal.create()
12
12
  local _dropSignal = Signal.create()
13
13
 
14
- -- Public --------------------------------------------------------------
14
+ -- Private ----------------------------------------------------------------
15
+
16
+ --[[
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
+ ]]
21
+ local function runChain(signal: any, value: any, name: string, player: Player?): any
22
+ local count = signal._count[1]
23
+ if count == 0 then
24
+ return value
25
+ end
26
+
27
+ local entries = signal._entries
28
+ local snapshot = table.create(count)
29
+ table.move(entries, 1, count, 1, snapshot)
30
+
31
+ local result = value
32
+ for i = 1, count do
33
+ local entry = snapshot[i]
34
+ if entry._state ~= 0 then
35
+ local returned = entry.fn(result, name, player)
36
+ if returned ~= nil then
37
+ result = returned
38
+ end
39
+ end
40
+ end
41
+ return result
42
+ end
43
+
44
+ -- Public -----------------------------------------------------------------
15
45
 
16
46
  local Middleware = {}
17
47
 
@@ -31,52 +61,12 @@ function Middleware.onDrop(
31
61
  return _dropSignal:connect(fn)
32
62
  end
33
63
 
34
- --[[
35
- Runs send middleware chain. Returns transformed data, or DROP sentinel.
36
- If no hooks, returns data unchanged (zero overhead).
37
- ]]
38
64
  function Middleware.runSend(data: any, name: string, player: Player?): any
39
- local count = _sendSignal._count[1]
40
- if count == 0 then
41
- return data
42
- end
43
-
44
- local entries = _sendSignal._entries
45
- local result = data
46
-
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
55
- end
56
-
57
- return result
65
+ return runChain(_sendSignal, data, name, player)
58
66
  end
59
67
 
60
68
  function Middleware.runReceive(data: any, name: string, player: Player?): any
61
- local count = _receiveSignal._count[1]
62
- if count == 0 then
63
- return data
64
- end
65
-
66
- local entries = _receiveSignal._entries
67
- local result = data
68
-
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
77
- end
78
-
79
- return result
69
+ return runChain(_receiveSignal, data, name, player)
80
70
  end
81
71
 
82
72
  function Middleware.fireDrop(player: Player, reason: string, name: string, data: any?): ()
@@ -84,11 +74,11 @@ function Middleware.fireDrop(player: Player, reason: string, name: string, data:
84
74
  end
85
75
 
86
76
  function Middleware.hasSendHooks(): boolean
87
- return _sendSignal._count[1] > 0
77
+ return _sendSignal:hasListeners()
88
78
  end
89
79
 
90
80
  function Middleware.hasReceiveHooks(): boolean
91
- return _receiveSignal._count[1] > 0
81
+ return _receiveSignal:hasListeners()
92
82
  end
93
83
 
94
84
  return table.freeze(Middleware)
@@ -1,33 +1,33 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Stack-based ChannelState pool for per-player channel reuse.
3
+ -- Stack-based ChannelState pool. Reset-on-acquire keeps released
4
+ -- channels reusable without per-release reset cost.
4
5
 
5
6
  local Channel = require(script.Parent.Channel)
6
7
  local Types = require(script.Parent.Parent.Types)
7
8
 
8
- -- Constants -----------------------------------------------------------
9
+ -- Constants --------------------------------------------------------------
9
10
 
10
11
  local DEFAULT_SIZE = 16
11
12
 
12
- -- State ---------------------------------------------------------------
13
+ -- State ------------------------------------------------------------------
13
14
 
14
15
  local _stack: { Types.ChannelState } = {}
15
16
  local _depth = 0
16
17
  local _maxSize = DEFAULT_SIZE
17
18
 
18
- -- Public --------------------------------------------------------------
19
+ -- Public -----------------------------------------------------------------
19
20
 
20
21
  local Pool = {}
21
22
 
22
23
  function Pool.acquire(): Types.ChannelState
23
24
  if _depth > 0 then
24
25
  local ch = _stack[_depth]
25
- _stack[_depth] = nil :: any
26
+ _stack[_depth] = nil
26
27
  _depth -= 1
27
28
  Channel.reset(ch)
28
29
  return ch
29
30
  end
30
-
31
31
  return Channel.create()
32
32
  end
33
33
 
@@ -35,17 +35,17 @@ function Pool.release(ch: Types.ChannelState): ()
35
35
  if _depth >= _maxSize then
36
36
  return
37
37
  end
38
-
39
38
  _depth += 1
40
39
  _stack[_depth] = ch
41
40
  end
42
41
 
43
42
  function Pool.setMaxSize(size: number): ()
43
+ if size < 0 then
44
+ error(`[Lync] Pool.setMaxSize: size must be >= 0, got {size}`)
45
+ end
44
46
  _maxSize = size
45
-
46
- -- Trim excess
47
47
  while _depth > _maxSize do
48
- _stack[_depth] = nil :: any
48
+ _stack[_depth] = nil
49
49
  _depth -= 1
50
50
  end
51
51
  end