@axpecter/lync 2.0.0 → 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 +475 -426
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  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 +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  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 +28 -21
  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 -36
  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 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,333 +1,301 @@
1
1
  --!strict
2
2
  --!native
3
- -- Per-channel buffer state with batching, delta cache, frame XOR.
3
+ -- Per-channel buffer state with MSB batch framing, XOR, timestamps.
4
4
 
5
5
  local Types = require(script.Parent.Parent.Types)
6
6
 
7
- type ChannelState = Types.ChannelState
8
-
9
7
  -- Constants -----------------------------------------------------------
10
8
 
11
- local INITIAL_SIZE = 1024
12
- local MAX_SIZE = 262144
13
- local MAX_BATCH = 65535
14
-
15
- local min = math.min
16
- local bxor = bit32.bxor
17
- local band32 = bit32.band
18
- local countlz = bit32.countlz
19
- local lshift = bit32.lshift
20
- local bor = bit32.bor
21
- local rshift = bit32.rshift
22
-
23
9
  local TS_NONE = 0
24
10
  local TS_FRAME = 1
25
11
  local TS_OFFSET = 2
26
12
  local TS_FULL = 3
27
13
 
28
- -- State ---------------------------------------------------------------
14
+ local DEFAULT_MAX = 262144
15
+ local EMPTY_REFS = table.freeze({})
29
16
 
30
- local _maxSize = MAX_SIZE
31
- local _currentPacket = ""
17
+ -- State ---------------------------------------------------------------
32
18
 
33
- -- Timestamp state updated once per flush via updateTimestamp().
34
- local _frameCounter: number = 0
35
- local _offsetMs: number = 0
36
- local _clockTime: number = 0
19
+ local _maxSize = DEFAULT_MAX
37
20
  local _anyTimestamp = false
38
-
39
- -- Stats gating. Default off; set via Lync.enableStats().
40
21
  local _statsEnabled = false
22
+ local _frameCounter = 0
23
+ local _offsetMs = 0
24
+ local _clock = 0.0
41
25
 
42
- -- Public --------------------------------------------------------------
43
-
44
- local Channel = {}
45
-
46
- function Channel.setMaxSize(bytes: number): ()
47
- if bytes < 4096 or bytes > 1048576 then
48
- error(`[Lync] Channel max size must be 4096-1048576, got {bytes}`)
49
- end
50
- _maxSize = bytes
51
- end
52
-
53
- function Channel.getMaxSize(): number
54
- return _maxSize
55
- end
26
+ -- Private -------------------------------------------------------------
56
27
 
57
- function Channel.create(): ChannelState
58
- return {
59
- buff = buffer.create(INITIAL_SIZE),
60
- cursor = 0,
61
- size = INITIAL_SIZE,
62
- refs = {},
63
- lastId = -1,
64
- countPos = 0,
65
- itemCount = 0,
66
- deltas = {},
67
- prevDump = nil,
68
- }
69
- end
28
+ local countlz = bit32.countlz
29
+ local lshift = bit32.lshift
30
+ local bxor = bit32.bxor
31
+ local bor = bit32.bor
32
+ local band = bit32.band
33
+ local writeu8 = buffer.writeu8
34
+ local writeu16 = buffer.writeu16
35
+ local writef64 = buffer.writef64
36
+ local readu8 = buffer.readu8
37
+ local readu32 = buffer.readu32
38
+ local round = math.round
39
+ local min = math.min
70
40
 
71
- function Channel.alloc(ch: ChannelState, bytes: number): ()
41
+ local function alloc(ch: Types.ChannelState, bytes: number): ()
72
42
  local needed = ch.cursor + bytes
73
43
  if needed <= ch.size then
74
44
  return
75
45
  end
76
46
  if needed > _maxSize then
77
47
  error(
78
- `[Lync] Channel overflow writing "{_currentPacket}": {needed} bytes > {_maxSize} max(call Channel.setMaxSize to raise)`
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)`
79
51
  )
80
52
  end
81
-
82
53
  local newSize = lshift(1, 32 - countlz(needed - 1))
83
-
84
54
  local newBuff = buffer.create(newSize)
85
55
  buffer.copy(newBuff, 0, ch.buff, 0, ch.cursor)
86
56
  ch.buff = newBuff
87
57
  ch.size = newSize
88
58
  end
89
59
 
90
- local alloc = Channel.alloc
91
-
92
- -- Seals the open batch header and returns a snapshot of the buffer + refs.
93
- function Channel.sealAndDump(ch: ChannelState): (buffer, { Instance })
94
- if ch.lastId >= 0 then
95
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
96
- ch.lastId = -1
97
- ch.countPos = 0
98
- ch.itemCount = 0
99
- end
100
-
101
- local cur = ch.cursor
102
- local out = buffer.create(cur)
103
- buffer.copy(out, 0, ch.buff, 0, cur)
104
-
105
- local refs = ch.refs
106
- return out, if #refs > 0 then table.clone(refs) else {}
107
- end
108
-
109
- function Channel.setCurrentPacket(name: string): ()
110
- _currentPacket = name
111
- end
112
-
113
- function Channel.updateTimestamp(frameCounter: number, offsetMs: number, clockTime: number): ()
114
- _frameCounter = frameCounter
115
- _offsetMs = offsetMs
116
- _clockTime = clockTime
117
- end
60
+ -- Public --------------------------------------------------------------
118
61
 
119
- function Channel.enableTimestamps(): ()
120
- _anyTimestamp = true
121
- end
62
+ local Channel = {}
122
63
 
123
- function Channel.hasTimestamps(): boolean
124
- return _anyTimestamp
125
- end
64
+ Channel.TS_NONE = TS_NONE
65
+ Channel.TS_FRAME = TS_FRAME
66
+ Channel.TS_OFFSET = TS_OFFSET
67
+ Channel.TS_FULL = TS_FULL
126
68
 
127
- function Channel.enableStats(): ()
128
- _statsEnabled = true
69
+ function Channel.create(): Types.ChannelState
70
+ return {
71
+ buff = buffer.create(1024),
72
+ cursor = 0,
73
+ size = 1024,
74
+ refs = {},
75
+ refCount = 0,
76
+ lastId = -1,
77
+ countPos = -1,
78
+ itemCount = 0,
79
+ singleMode = false,
80
+ singlePos = 0,
81
+ deltas = {},
82
+ prevDump = nil,
83
+ prevDumpLen = 0,
84
+ currentPacket = nil,
85
+ }
129
86
  end
130
87
 
131
- function Channel.statsEnabled(): boolean
132
- return _statsEnabled
88
+ function Channel.alloc(ch: Types.ChannelState, bytes: number): ()
89
+ alloc(ch, bytes)
133
90
  end
134
91
 
135
- -- Batch opener: no timestamp variant. Resolved at define time via reg._openFn (#2)
136
- local function openBatchPlain(ch: ChannelState, id: number, name: string): ()
137
- if ch.lastId >= 0 then
138
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
139
- end
140
- _currentPacket = name
141
- alloc(ch, 3)
142
- local c = ch.cursor
143
- buffer.writeu8(ch.buff, c, id)
144
- ch.countPos = c + 1
145
- ch.cursor = c + 3
146
- ch.lastId = id
92
+ function Channel.reset(ch: Types.ChannelState): ()
93
+ ch.cursor = 0
94
+ ch.lastId = -1
95
+ ch.countPos = -1
147
96
  ch.itemCount = 0
148
- end
149
-
150
- local function openBatchFrame(ch: ChannelState, id: number, name: string): ()
151
- if ch.lastId >= 0 then
152
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
97
+ ch.singleMode = false
98
+ ch.singlePos = 0
99
+ ch.currentPacket = nil
100
+ if ch.refCount > 0 then
101
+ table.clear(ch.refs)
102
+ ch.refCount = 0
153
103
  end
154
- _currentPacket = name
155
- alloc(ch, 4)
156
- local c = ch.cursor
157
- buffer.writeu8(ch.buff, c, id)
158
- ch.countPos = c + 1
159
- buffer.writeu8(ch.buff, c + 3, _frameCounter)
160
- ch.cursor = c + 4
161
- ch.lastId = id
162
- ch.itemCount = 0
163
104
  end
164
105
 
165
- local function openBatchOffset(ch: ChannelState, id: number, name: string): ()
166
- if ch.lastId >= 0 then
167
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
106
+ --[[
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.
111
+ ]]
112
+ function Channel.resolveOpenFn(
113
+ timestampMode: number,
114
+ name: string
115
+ ): (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
175
+ 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
195
+ end
168
196
  end
169
- _currentPacket = name
170
- alloc(ch, 5)
171
- local c = ch.cursor
172
- buffer.writeu8(ch.buff, c, id)
173
- ch.countPos = c + 1
174
- buffer.writeu16(ch.buff, c + 3, _offsetMs)
175
- ch.cursor = c + 5
176
- ch.lastId = id
177
- ch.itemCount = 0
178
197
  end
179
198
 
180
- local function openBatchFull(ch: ChannelState, id: number, name: string): ()
181
- if ch.lastId >= 0 then
182
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
183
- end
184
- _currentPacket = name
185
- alloc(ch, 11)
186
- local c = ch.cursor
187
- buffer.writeu8(ch.buff, c, id)
188
- ch.countPos = c + 1
189
- buffer.writef64(ch.buff, c + 3, _clockTime)
190
- ch.cursor = c + 11
191
- ch.lastId = id
192
- ch.itemCount = 0
193
- end
199
+ --[[
200
+ Writes one item into a batch. Handles MSB single→multi upgrade.
201
+ ]]
202
+ function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
203
+ if reg.id ~= ch.lastId or ch.itemCount >= 0xFFFF then
204
+ reg._openFn(ch, reg.id)
205
+ 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
+ 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
216
+
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
+ ]]
222
+ local cursor = ch.cursor
223
+ local payloadLen = cursor - afterTs
224
+ if cursor + 2 > ch.size then
225
+ alloc(ch, 2)
226
+ end
227
+ local b2 = ch.buff
194
228
 
195
- -- Returns the correct openBatch function for a given timestamp mode.
196
- function Channel.resolveOpenFn(tsMode: number): (ch: ChannelState, id: number, name: string) -> ()
197
- if tsMode == TS_FRAME then
198
- return openBatchFrame
199
- elseif tsMode == TS_OFFSET then
200
- return openBatchOffset
201
- elseif tsMode == TS_FULL then
202
- return openBatchFull
203
- end
204
- return openBatchPlain
205
- end
229
+ if payloadLen > 0 then
230
+ buffer.copy(b2, afterTs + 2, b2, afterTs, payloadLen)
231
+ end
206
232
 
207
- -- Writes a single value into the batch. Zero branching on the hot path.
208
- function Channel.writeBatch(ch: ChannelState, reg: any, data: any): ()
209
- local id = reg.id
210
- if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
211
- reg._openFn(ch, id, reg.name)
233
+ ch.countPos = afterTs
234
+ writeu16(b2, afterTs, 0) -- placeholder u16; sealAndDump writes final count
235
+ ch.cursor = cursor + 2
236
+ ch.singleMode = false
212
237
  end
213
238
 
214
239
  ch.itemCount += 1
215
240
  reg.codec.write(ch, data)
216
241
  end
217
242
 
218
- -- Appends pre-serialized bytes into a batch. Used by broadcast fan-out.
219
- function Channel.writeBatchRaw(
220
- ch: ChannelState,
221
- reg: any,
222
- src: buffer,
223
- srcLen: number,
224
- srcRefs: { Instance }?
225
- ): ()
226
- local id = reg.id
227
- if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
228
- reg._openFn(ch, id, reg.name)
243
+ --[[
244
+ Seals the current batch and returns a snapshot for transmission.
245
+ ]]
246
+ function Channel.sealAndDump(ch: Types.ChannelState): (buffer, { Instance }, number, number)
247
+ -- Multi-item batches need their count finalized before transmission
248
+ if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
249
+ writeu16(ch.buff, ch.countPos, ch.itemCount)
229
250
  end
230
251
 
231
- ch.itemCount += 1
232
-
233
- if srcLen > 0 then
234
- alloc(ch, srcLen)
235
- buffer.copy(ch.buff, ch.cursor, src, 0, srcLen)
236
- ch.cursor += srcLen
237
- end
252
+ local cursor = ch.cursor
253
+ local snapshot = buffer.create(cursor)
254
+ buffer.copy(snapshot, 0, ch.buff, 0, cursor)
238
255
 
239
- if srcRefs then
240
- local refs = ch.refs
241
- table.move(srcRefs, 1, #srcRefs, #refs + 1, refs)
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
242
262
  end
243
- end
244
263
 
245
- local function sealOpen(ch: ChannelState): ()
246
- if ch.lastId >= 0 then
247
- buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
248
- ch.lastId = -1
249
- ch.countPos = 0
250
- ch.itemCount = 0
251
- end
264
+ return snapshot, refs, cursor, refCount
252
265
  end
253
266
 
254
267
  --[[
255
- Writes a query frame. Seals any open batch, then writes:
256
- id(u8) + correlationId(varint) + status(u8)
257
- Status 0 = payload follows. Status 1 = nil response.
268
+ XOR current frame against previous for deflate-friendly output.
258
269
  ]]
259
- function Channel.writeQuery(
260
- ch: ChannelState,
261
- id: number,
262
- name: string,
263
- correlationId: number,
264
- codec: any?,
265
- data: any?
266
- ): ()
267
- _currentPacket = name
268
- sealOpen(ch)
269
-
270
- alloc(ch, 2)
271
- local c = ch.cursor
272
- buffer.writeu8(ch.buff, c, id)
273
- ch.cursor = c + 1
274
-
275
- -- Inline varint write to avoid circular dependency (Varint requires Channel)
276
- local v = correlationId
277
- if v < 0x80 then
278
- alloc(ch, 1)
279
- buffer.writeu8(ch.buff, ch.cursor, v)
280
- ch.cursor += 1
281
- else
282
- alloc(ch, 5)
283
- local b = ch.buff
284
- local vc = ch.cursor
285
- while v >= 0x80 do
286
- buffer.writeu8(b, vc, bor(band32(v, 0x7F), 0x80))
287
- v = rshift(v, 7)
288
- vc += 1
289
- end
290
- buffer.writeu8(b, vc, v)
291
- ch.cursor = vc + 1
292
- end
293
-
294
- local c2 = ch.cursor
295
- alloc(ch, 1)
296
- if codec then
297
- buffer.writeu8(ch.buff, c2, 0)
298
- ch.cursor = c2 + 1
299
- codec.write(ch, data)
300
- else
301
- buffer.writeu8(ch.buff, c2, 1)
302
- ch.cursor = c2 + 1
303
- end
304
- end
305
-
306
- -- XOR current frame against previous. Produces zero-heavy output for deflate.
307
- function Channel.xorApply(current: buffer, previous: buffer?): buffer
308
- local curLen = buffer.len(current)
270
+ function Channel.xorApply(
271
+ current: buffer,
272
+ curLen: number,
273
+ previous: buffer?,
274
+ prevLen: number
275
+ ): buffer
309
276
  if not previous then
310
277
  return current
311
278
  end
312
279
 
313
- local prevLen = buffer.len(previous :: buffer)
314
- if prevLen == 0 then
315
- return current
316
- end
317
-
318
280
  local result = buffer.create(curLen)
319
281
  local overlap = min(curLen, prevLen)
320
282
  local prev = previous :: buffer
321
283
 
322
- local aligned = band32(overlap, 0xFFFFFFFC)
323
- for i = 0, aligned - 4, 4 do
324
- buffer.writeu32(result, i, bxor(buffer.readu32(current, i), buffer.readu32(prev, i)))
284
+ -- u32 chunks
285
+ local aligned = overlap - (overlap % 4)
286
+ local i = 0
287
+ while i < aligned do
288
+ buffer.writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
289
+ i += 4
325
290
  end
326
291
 
327
- for i = aligned, overlap - 1 do
328
- buffer.writeu8(result, i, bxor(buffer.readu8(current, i), buffer.readu8(prev, i)))
292
+ -- u8 remainder of overlap
293
+ while i < overlap do
294
+ writeu8(result, i, bxor(readu8(current, i), readu8(prev, i)))
295
+ i += 1
329
296
  end
330
297
 
298
+ -- Excess bytes (current longer than previous)
331
299
  if curLen > overlap then
332
300
  buffer.copy(result, overlap, current, overlap, curLen - overlap)
333
301
  end
@@ -335,36 +303,123 @@ function Channel.xorApply(current: buffer, previous: buffer?): buffer
335
303
  return result
336
304
  end
337
305
 
338
- -- Conditional refs clear (#6)
339
- function Channel.reset(ch: ChannelState): ()
340
- ch.cursor = 0
306
+ --[[
307
+ Writes a query frame with MSB encoding for nil-status.
308
+ MSB set = nil response, MSB clear = payload follows.
309
+ ]]
310
+ function Channel.writeQuery(
311
+ ch: Types.ChannelState,
312
+ id: number,
313
+ correlationId: number,
314
+ codec: Types.InternalCodec<any>?,
315
+ data: any
316
+ ): ()
317
+ -- Queries are standalone frames, so close any open packet batch first
318
+ if ch.lastId >= 0 and not ch.singleMode and ch.countPos >= 0 then
319
+ writeu16(ch.buff, ch.countPos, ch.itemCount)
320
+ end
341
321
  ch.lastId = -1
342
- ch.countPos = 0
343
- ch.itemCount = 0
344
- if #ch.refs > 0 then
345
- table.clear(ch.refs)
322
+ ch.singleMode = false
323
+
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
358
+
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
346
394
  end
347
395
  end
348
396
 
349
- -- Test helper: builds a minimal reg-like object for writeBatch.
350
- function Channel.mockReg(id: number, name: string, codec: any): any
351
- return {
352
- id = id,
353
- name = name,
354
- codec = codec,
355
- timestampMode = 0,
356
- _openFn = openBatchPlain,
357
- drops = 0,
358
- bytesSent = 0,
359
- bytesReceived = 0,
360
- fires = 0,
361
- recvFires = 0,
362
- }
397
+ -- Configuration -------------------------------------------------------
398
+
399
+ function Channel.setMaxSize(bytes: number): ()
400
+ _maxSize = bytes
363
401
  end
364
402
 
365
- Channel.TS_NONE = TS_NONE
366
- Channel.TS_FRAME = TS_FRAME
367
- Channel.TS_OFFSET = TS_OFFSET
368
- Channel.TS_FULL = TS_FULL
403
+ function Channel.updateTimestamp(): ()
404
+ _frameCounter = (_frameCounter + 1) % 256
405
+ _offsetMs = round((os.clock() % 65.536) * 1000)
406
+ _clock = os.clock()
407
+ end
408
+
409
+ function Channel.enableTimestamps(): ()
410
+ _anyTimestamp = true
411
+ end
412
+
413
+ function Channel.hasTimestamps(): boolean
414
+ return _anyTimestamp
415
+ end
416
+
417
+ function Channel.enableStats(): ()
418
+ _statsEnabled = true
419
+ end
420
+
421
+ function Channel.statsEnabled(): boolean
422
+ return _statsEnabled
423
+ end
369
424
 
370
425
  return table.freeze(Channel)