@axpecter/lync 2.2.1 → 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 +68 -54
  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 +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 +109 -103
  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
@@ -1,10 +1,12 @@
1
1
  --!strict
2
2
  --!native
3
- -- Struct and deltaStruct codecs.
3
+ -- struct() and deltaStruct(). Bool fields bit-pack into a tail block.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
6
7
  local Channel = require(script.Parent.Parent.Parent.internal.Channel)
7
- local Shared = require(script.Parent.Shared)
8
+ local Log = require(script.Parent.Parent.Parent.util.Log)
9
+ local Shared = require(script.Parent.Parent.composite.Shared)
8
10
  local Types = require(script.Parent.Parent.Parent.Types)
9
11
 
10
12
  -- Private ----------------------------------------------------------------
@@ -12,10 +14,41 @@ local Types = require(script.Parent.Parent.Parent.Types)
12
14
  local alloc = Base.alloc
13
15
  local writeByte = Channel.writeByte
14
16
  local writeu8 = buffer.writeu8
17
+ local writeu16 = buffer.writeu16
18
+ local writeu32 = buffer.writeu32
15
19
  local readu8 = buffer.readu8
20
+ local readu16 = buffer.readu16
21
+ local readu32 = buffer.readu32
16
22
  local band = bit32.band
17
23
  local bor = bit32.bor
18
24
  local lshift = bit32.lshift
25
+ local rshift = bit32.rshift
26
+ local countrz = bit32.countrz
27
+
28
+ -- 1/2/4-byte direct writes; 3-byte case splits into u16 + u8 (no native u24).
29
+ local function writeBitmap(b: buffer, off: number, mask: number, bitmapBytes: number): ()
30
+ if bitmapBytes == 1 then
31
+ writeu8(b, off, mask)
32
+ elseif bitmapBytes == 2 then
33
+ writeu16(b, off, mask)
34
+ elseif bitmapBytes == 4 then
35
+ writeu32(b, off, mask)
36
+ else
37
+ writeu16(b, off, band(mask, 0xFFFF))
38
+ writeu8(b, off + 2, band(rshift(mask, 16), 0xFF))
39
+ end
40
+ end
41
+
42
+ local function readBitmap(b: buffer, off: number, bitmapBytes: number): number
43
+ if bitmapBytes == 1 then
44
+ return readu8(b, off)
45
+ elseif bitmapBytes == 2 then
46
+ return readu16(b, off)
47
+ elseif bitmapBytes == 4 then
48
+ return readu32(b, off)
49
+ end
50
+ return bor(readu16(b, off), lshift(readu8(b, off + 2), 16))
51
+ end
19
52
 
20
53
  local extractFields = Shared.extractFields
21
54
  local packBools = Shared.packBools
@@ -27,6 +60,7 @@ local releaseScratch = Shared.releaseScratch
27
60
  local allocDeltaId = Shared.allocDeltaId
28
61
  local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
29
62
  local DELTA_FULL_OFFSET = Shared.DELTA_FULL_OFFSET
63
+ local DELTA_MULTI_OFFSET = Shared.DELTA_MULTI_OFFSET
30
64
 
31
65
  local function buildFrozenSchema(
32
66
  dataKeys: { string },
@@ -53,16 +87,23 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
53
87
  local dataKeys, dataCodecs, boolKeys = extractFields(schema)
54
88
  local dataCount = #dataKeys
55
89
  local boolCount = #boolKeys
56
- local boolBytes = (boolCount + 7) // 8
90
+ local boolBytes = Shared.boolBytes(boolCount)
57
91
 
92
+ -- Single pass: detect all-direct fast-write path + propagate _hasUnknown.
58
93
  local allDirect, fixedSize = true, 0
94
+ local hasUnknown = false
59
95
  for i = 1, dataCount do
60
96
  local c = dataCodecs[i]
61
- if not c._size or not c._directWrite or not c._directRead then
62
- allDirect = false
63
- break
97
+ if c._hasUnknown then
98
+ hasUnknown = true
99
+ end
100
+ if allDirect then
101
+ if c._size and c._directWrite and c._directRead then
102
+ fixedSize += c._size
103
+ else
104
+ allDirect = false
105
+ end
64
106
  end
65
- fixedSize += c._size
66
107
  end
67
108
 
68
109
  local totalFixed = fixedSize + boolBytes
@@ -72,19 +113,19 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
72
113
  local offsets = table.create(dataCount)
73
114
  local directWrites = table.create(dataCount)
74
115
  local directReads = table.create(dataCount)
75
- local cursor = 0
116
+ local runningOffset = 0
76
117
  for i = 1, dataCount do
77
118
  local c = dataCodecs[i]
78
- offsets[i] = cursor
119
+ offsets[i] = runningOffset
79
120
  directWrites[i] = c._directWrite
80
121
  directReads[i] = c._directRead
81
- cursor += c._size :: number
122
+ runningOffset += c._size :: number
82
123
  end
83
124
  table.freeze(offsets)
84
125
  table.freeze(directWrites)
85
126
  table.freeze(directReads)
86
127
 
87
- return table.freeze({
128
+ local codec: Types.InternalCodec<any> = {
88
129
  _size = totalFixed,
89
130
  _schema = frozenSchema,
90
131
 
@@ -96,6 +137,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
96
137
  return
97
138
  end
98
139
 
140
+ -- Inline bool packing to avoid the cursor-based packBools call.
99
141
  local boolOff = off + fixedSize
100
142
  local idx = 1
101
143
  for byteIdx = 0, boolBytes - 1 do
@@ -124,15 +166,15 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
124
166
  end,
125
167
 
126
168
  write = function(ch: Types.ChannelState, value: any): ()
127
- local c = ch.cursor
128
- if c + totalFixed > ch.size then
169
+ local cursor = ch.cursor
170
+ if cursor + totalFixed > ch.size then
129
171
  alloc(ch, totalFixed)
130
172
  end
131
173
  local b = ch.buff
132
174
  for i = 1, dataCount do
133
- directWrites[i](b, c + offsets[i], value[dataKeys[i]])
175
+ directWrites[i](b, cursor + offsets[i], value[dataKeys[i]])
134
176
  end
135
- ch.cursor = c + fixedSize
177
+ ch.cursor = cursor + fixedSize
136
178
  if boolCount > 0 then
137
179
  packBools(ch, value, boolKeys, boolCount)
138
180
  end
@@ -148,10 +190,14 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
148
190
  end
149
191
  return result, totalFixed
150
192
  end,
151
- }) :: Types.InternalCodec<any>
193
+ }
194
+ if hasUnknown then
195
+ codec._hasUnknown = true
196
+ end
197
+ return table.freeze(codec)
152
198
  end
153
199
 
154
- return table.freeze({
200
+ local codec: Types.InternalCodec<any> = {
155
201
  _schema = frozenSchema,
156
202
 
157
203
  write = function(ch: Types.ChannelState, value: any): ()
@@ -176,66 +222,261 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
176
222
  end
177
223
  return result, total
178
224
  end,
179
- }) :: Types.InternalCodec<any>
225
+ }
226
+ if hasUnknown then
227
+ codec._hasUnknown = true
228
+ end
229
+ return table.freeze(codec)
180
230
  end
181
231
 
232
+ --[[
233
+ Per-field delta encoder. Splits a struct into segments (one per data
234
+ field, plus one for the packed-bool block if any) and emits only changed
235
+ segments. Wire flag byte:
236
+ 0 UNCHANGED (reuse cache)
237
+ [1 .. segCount] single segment k changed
238
+ segCount + DELTA_FULL_OFFSET FULL payload
239
+ segCount + DELTA_MULTI_OFFSET MULTI: bitmap + dirty segs in order
240
+ ]]
182
241
  function Struct.deltaStruct(
183
242
  schema: { [string]: Types.InternalCodec<any> }
184
243
  ): Types.InternalCodec<any>
185
- local dataKeys, _dataCodecs, boolKeys = extractFields(schema)
186
- local segCount = #dataKeys + (if #boolKeys > 0 then 1 else 0)
244
+ local dataKeys, dataCodecs, boolKeys = extractFields(schema)
245
+ local dataCount = #dataKeys
246
+ local boolCount = #boolKeys
247
+ local hasBoolSeg = boolCount > 0
248
+ local segCount = dataCount + (if hasBoolSeg then 1 else 0)
187
249
  local FLAG_FULL = segCount + DELTA_FULL_OFFSET
250
+ local FLAG_MULTI = segCount + DELTA_MULTI_OFFSET
251
+ local boolSegIdx = segCount
252
+
253
+ if FLAG_MULTI > 0xFF then
254
+ Log.error(`too many segments ({segCount}); flag byte would overflow`)
255
+ end
256
+ if segCount > 32 then
257
+ Log.error(`dirty bitmask is u32, segCount {segCount} exceeds 32`)
258
+ end
259
+
188
260
  local deltaId = allocDeltaId()
189
- local frozenSchema = buildFrozenSchema(dataKeys, boolKeys, schema)
190
261
  local innerStruct = Struct.struct(schema)
262
+ -- Reuse innerStruct's frozen schema; avoids a second buildFrozenSchema.
263
+ local frozenSchema = innerStruct._schema :: { [string]: Types.InternalCodec<any> }
264
+
265
+ -- All-direct + fixed-size: segment offsets/lengths are statically known.
266
+ local allDirect = dataCount > 0
267
+ local fixedSize = 0
268
+ for i = 1, dataCount do
269
+ local c = dataCodecs[i]
270
+ if not c._size or not c._directWrite or not c._directRead then
271
+ allDirect = false
272
+ break
273
+ end
274
+ fixedSize += c._size :: number
275
+ end
276
+
277
+ local boolBytes = Shared.boolBytes(boolCount)
278
+ local bitmapBytes = Shared.boolBytes(segCount)
279
+
280
+ local staticSegOff: { number }? = nil
281
+ local staticSegLen: { number }? = nil
282
+ if allDirect then
283
+ local off = 0
284
+ local segOff = table.create(segCount)
285
+ local segLen = table.create(segCount)
286
+ for i = 1, dataCount do
287
+ segOff[i] = off
288
+ segLen[i] = dataCodecs[i]._size :: number
289
+ off += dataCodecs[i]._size :: number
290
+ end
291
+ if hasBoolSeg then
292
+ segOff[boolSegIdx] = off
293
+ segLen[boolSegIdx] = boolBytes
294
+ end
295
+ staticSegOff = table.freeze(segOff)
296
+ staticSegLen = table.freeze(segLen)
297
+ end
298
+
299
+ -- Apply one segment's bytes onto `target`. Returns bytes consumed.
300
+ local function applySegmentRead(
301
+ target: { [string]: any },
302
+ segIdx: number,
303
+ src: buffer,
304
+ pos: number,
305
+ refs: { Instance }?
306
+ ): number
307
+ if hasBoolSeg and segIdx == boolSegIdx then
308
+ return unpackBools(src, pos, boolKeys, boolCount, target)
309
+ end
310
+ local v, consumed = dataCodecs[segIdx].read(src, pos, refs)
311
+ target[dataKeys[segIdx]] = v
312
+ return consumed
313
+ end
314
+
315
+ -- Splice `src[srcOff..srcOff+len)` into the channel's main buffer.
316
+ local function writeBytes(ch: Types.ChannelState, src: buffer, srcOff: number, len: number): ()
317
+ if len == 0 then
318
+ return
319
+ end
320
+ local cursor = ch.cursor
321
+ if cursor + len > ch.size then
322
+ alloc(ch, len)
323
+ end
324
+ buffer.copy(ch.buff, cursor, src, srcOff, len)
325
+ ch.cursor = cursor + len
326
+ end
191
327
 
192
328
  return table.freeze({
193
329
  _isDelta = true,
194
330
  _schema = frozenSchema,
195
331
 
196
332
  write = function(ch: Types.ChannelState, value: any): ()
333
+ local scratch = acquireScratch()
334
+ local segOff: { number }, segLen: { number }
335
+
336
+ if staticSegOff then
337
+ -- Bulk write via innerStruct, constant offsets.
338
+ innerStruct.write(scratch, value)
339
+ segOff = staticSegOff :: { number }
340
+ segLen = staticSegLen :: { number }
341
+ else
342
+ segOff = table.create(segCount)
343
+ segLen = table.create(segCount)
344
+ local prev = 0
345
+ for i = 1, dataCount do
346
+ dataCodecs[i].write(scratch, value[dataKeys[i]])
347
+ local cur = scratch.cursor
348
+ segOff[i] = prev
349
+ segLen[i] = cur - prev
350
+ prev = cur
351
+ end
352
+ if hasBoolSeg then
353
+ packBools(scratch, value, boolKeys, boolCount)
354
+ segOff[boolSegIdx] = prev
355
+ segLen[boolSegIdx] = scratch.cursor - prev
356
+ end
357
+ end
358
+
359
+ local scratchLen = scratch.cursor
197
360
  local cache = ch.deltas[deltaId]
198
361
 
199
362
  if not cache then
200
363
  writeByte(ch, FLAG_FULL)
201
-
202
- local scratch = acquireScratch()
203
- innerStruct.write(scratch, value)
204
- local snapLen = scratch.cursor
205
-
206
- ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
207
- innerStruct.write(ch, value)
364
+ writeBytes(ch, scratch.buff, 0, scratchLen)
365
+ --[[
366
+ Variable-size case stores fresh segOff/segLen by ref;
367
+ static case stores the module-level frozen tables.
368
+ ]]
369
+ ch.deltas[deltaId] = {
370
+ raw = snapshot(scratch.buff, scratchLen),
371
+ len = scratchLen,
372
+ segOff = segOff,
373
+ segLen = segLen,
374
+ } :: any
208
375
  releaseScratch()
209
376
  return
210
377
  end
211
378
 
212
- local scratch = acquireScratch()
213
- innerStruct.write(scratch, value)
214
- local scratchLen = scratch.cursor
379
+ local cacheRaw = cache.raw
380
+ local cacheOff = (cache :: any).segOff :: { number }
381
+ local cacheLen = (cache :: any).segLen :: { number }
382
+
383
+ local dirtyMask = 0
384
+ local dirtyCount = 0
385
+ for i = 1, segCount do
386
+ local newLen = segLen[i]
387
+ if
388
+ newLen ~= cacheLen[i]
389
+ or not rangeEqual(scratch.buff, segOff[i], cacheRaw, cacheOff[i], newLen)
390
+ then
391
+ dirtyMask = bor(dirtyMask, lshift(1, i - 1))
392
+ dirtyCount += 1
393
+ end
394
+ end
215
395
 
216
- if
217
- scratchLen == cache.len
218
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
219
- then
396
+ if dirtyCount == 0 then
220
397
  writeByte(ch, DELTA_UNCHANGED)
221
398
  releaseScratch()
222
399
  return
223
400
  end
224
401
 
225
- writeByte(ch, FLAG_FULL)
226
- innerStruct.write(ch, value)
402
+ if dirtyCount == segCount then
403
+ writeByte(ch, FLAG_FULL)
404
+ writeBytes(ch, scratch.buff, 0, scratchLen)
405
+ elseif dirtyCount == 1 then
406
+ local segIdx = countrz(dirtyMask) + 1
407
+ writeByte(ch, segIdx)
408
+ writeBytes(ch, scratch.buff, segOff[segIdx], segLen[segIdx])
409
+ else
410
+ writeByte(ch, FLAG_MULTI)
411
+ local cursor = ch.cursor
412
+ if cursor + bitmapBytes > ch.size then
413
+ alloc(ch, bitmapBytes)
414
+ end
415
+ writeBitmap(ch.buff, cursor, dirtyMask, bitmapBytes)
416
+ ch.cursor = cursor + bitmapBytes
417
+ for i = 1, segCount do
418
+ if band(dirtyMask, lshift(1, i - 1)) ~= 0 then
419
+ writeBytes(ch, scratch.buff, segOff[i], segLen[i])
420
+ end
421
+ end
422
+ end
227
423
 
228
424
  cache.raw = snapshot(scratch.buff, scratchLen)
229
- cache.len = scratchLen
425
+ cache.len = scratchLen;
426
+ (cache :: any).segOff = segOff;
427
+ (cache :: any).segLen = segLen
230
428
  releaseScratch()
231
429
  end,
232
430
 
233
431
  read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
234
- if readu8(src, pos) == DELTA_UNCHANGED then
235
- return nil, 1
432
+ local flag = readu8(src, pos)
433
+
434
+ if flag == DELTA_UNCHANGED then
435
+ local cached = Baseline.getCache(deltaId)
436
+ if not cached then
437
+ Log.error("UNCHANGED flag before any FULL frame; baseline missing")
438
+ end
439
+ return cached, 1
440
+ end
441
+
442
+ if flag == FLAG_FULL then
443
+ local value, consumed = innerStruct.read(src, pos + 1, refs)
444
+ Baseline.setCache(deltaId, value)
445
+ return value, 1 + consumed
236
446
  end
237
- local value, consumed = innerStruct.read(src, pos + 1, refs)
238
- return value, 1 + consumed
447
+
448
+ if flag <= segCount then
449
+ local cached = Baseline.getCache(deltaId)
450
+ if cached == nil then
451
+ Log.error(
452
+ `per-field update (seg {flag}) before any FULL frame; baseline missing`
453
+ )
454
+ end
455
+ local newValue = table.clone(cached)
456
+ local consumed = applySegmentRead(newValue, flag, src, pos + 1, refs)
457
+ Baseline.setCache(deltaId, newValue)
458
+ return newValue, 1 + consumed
459
+ end
460
+
461
+ if flag == FLAG_MULTI then
462
+ local cached = Baseline.getCache(deltaId)
463
+ if not cached then
464
+ Log.error("multi-field update before any FULL frame")
465
+ end
466
+ local newValue = table.clone(cached)
467
+ local mask = readBitmap(src, pos + 1, bitmapBytes)
468
+ local total = 1 + bitmapBytes
469
+ for i = 1, segCount do
470
+ if band(mask, lshift(1, i - 1)) ~= 0 then
471
+ local consumed = applySegmentRead(newValue, i, src, pos + total, refs)
472
+ total += consumed
473
+ end
474
+ end
475
+ Baseline.setCache(deltaId, newValue)
476
+ return newValue, total
477
+ end
478
+
479
+ Log.error(`invalid delta flag {flag}`)
239
480
  end,
240
481
  }) :: Types.InternalCodec<any>
241
482
  end
@@ -1,8 +1,9 @@
1
1
  --!strict
2
- --!optimize 2
2
+ --!native
3
3
  -- Discriminated union codec. u8 variant tag + variant payload.
4
4
 
5
5
  local Channel = require(script.Parent.Parent.Parent.internal.Channel)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
 
8
9
  -- Private ----------------------------------------------------------------
@@ -18,10 +19,9 @@ function Tagged.define(
18
19
  tagField: string,
19
20
  variants: { [string]: Types.InternalCodec<any> }
20
21
  ): Types.InternalCodec<any>
21
- if tagField == "" then
22
- error("[Lync] Lync.tagged: tagField cannot be empty")
23
- end
22
+ Log.assert(tagField ~= "", "tagField cannot be empty")
24
23
 
24
+ -- Sort variant names so the wire-form tag id is stable across runs.
25
25
  local names: { string } = {}
26
26
  for name in variants do
27
27
  table.insert(names, name)
@@ -29,16 +29,13 @@ function Tagged.define(
29
29
  table.sort(names)
30
30
 
31
31
  local variantCount = #names
32
- if variantCount == 0 then
33
- error("[Lync] Lync.tagged: requires at least one variant")
34
- end
32
+ Log.assert(variantCount > 0, "requires at least one variant")
35
33
  if variantCount > 256 then
36
- error("[Lync] Lync.tagged: exceeds 256 variants")
34
+ Log.error(`exceeds 256 variants ({variantCount})`)
37
35
  end
38
36
 
39
37
  local nameToTag: { [string]: number } = {}
40
38
  local tagToCodec = table.create(variantCount)
41
- local tagToName = table.create(variantCount)
42
39
  local hasUnknown = false
43
40
 
44
41
  for i = 1, variantCount do
@@ -46,15 +43,14 @@ function Tagged.define(
46
43
  local codec = variants[name]
47
44
  nameToTag[name] = i - 1
48
45
  tagToCodec[i] = codec
49
- tagToName[i] = name
50
46
  if codec._hasUnknown then
51
47
  hasUnknown = true
52
48
  end
53
49
  end
54
50
 
51
+ table.freeze(names)
55
52
  table.freeze(nameToTag)
56
53
  table.freeze(tagToCodec)
57
- table.freeze(tagToName)
58
54
 
59
55
  local codec: Types.InternalCodec<any> = {
60
56
  _isTagged = true,
@@ -64,11 +60,11 @@ function Tagged.define(
64
60
  write = function(ch: Types.ChannelState, value: any): ()
65
61
  local name = value[tagField]
66
62
  if name == nil then
67
- error(`[Lync] Tagged.write: missing tagField "{tagField}"`)
63
+ Log.error(`missing tagField "{tagField}"`)
68
64
  end
69
65
  local tag = nameToTag[name]
70
66
  if tag == nil then
71
- error(`[Lync] Tagged.write: unknown variant "{name}"`)
67
+ Log.error(`unknown variant "{name}"`)
72
68
  end
73
69
 
74
70
  writeByte(ch, tag)
@@ -79,12 +75,13 @@ function Tagged.define(
79
75
  local tag = readu8(src, pos)
80
76
  local varCodec = tagToCodec[tag + 1]
81
77
  if not varCodec then
82
- error(`[Lync] Tagged.read: unknown tag {tag}`)
78
+ Log.error(`unknown tag {tag}`)
83
79
  end
84
80
 
85
81
  local data, consumed = varCodec.read(src, pos + 1, refs)
86
- if type(data) == "table" then
87
- data[tagField] = tagToName[tag + 1]
82
+ -- Re-attach the discriminator so callers can dispatch on it.
83
+ if typeof(data) == "table" then
84
+ data[tagField] = names[tag + 1]
88
85
  end
89
86
  return data, 1 + consumed
90
87
  end,
@@ -1,8 +1,9 @@
1
1
  --!strict
2
- --!optimize 2
2
+ --!native
3
3
  -- Positional ordered codec. Like struct but indexed.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
 
8
9
  -- Private ----------------------------------------------------------------
@@ -16,36 +17,38 @@ local Tuple = {}
16
17
  function Tuple.define(...: Types.InternalCodec<any>): Types.InternalCodec<any>
17
18
  local codecs = { ... }
18
19
  local count = #codecs
19
- if count == 0 then
20
- error("[Lync] Lync.tuple: requires at least one codec")
21
- end
20
+ Log.assert(count > 0, "requires at least one codec")
22
21
 
22
+ -- Single pass: detect all-direct fast path + propagate _hasUnknown.
23
23
  local allDirect, totalSize, hasUnknown = true, 0, false
24
24
  for i = 1, count do
25
25
  local c = codecs[i]
26
26
  if c._hasUnknown then
27
27
  hasUnknown = true
28
28
  end
29
- if not c._size or not c._directWrite or not c._directRead then
30
- allDirect = false
31
- elseif allDirect then
32
- totalSize += c._size
29
+ if allDirect then
30
+ if c._size and c._directWrite and c._directRead then
31
+ totalSize += c._size
32
+ else
33
+ allDirect = false
34
+ end
33
35
  end
34
36
  end
35
37
  table.freeze(codecs)
36
38
 
39
+ -- All-direct: pre-compute offsets so write/read avoids the cursor dance per element.
37
40
  if allDirect then
38
41
  local offsets = table.create(count)
39
42
  local directWrites = table.create(count)
40
43
  local directReads = table.create(count)
41
- local cursor = 0
44
+ local runningOffset = 0
42
45
 
43
46
  for i = 1, count do
44
47
  local c = codecs[i]
45
- offsets[i] = cursor
48
+ offsets[i] = runningOffset
46
49
  directWrites[i] = c._directWrite
47
50
  directReads[i] = c._directRead
48
- cursor += c._size :: number
51
+ runningOffset += c._size :: number
49
52
  end
50
53
  table.freeze(offsets)
51
54
  table.freeze(directWrites)
@@ -71,15 +74,15 @@ function Tuple.define(...: Types.InternalCodec<any>): Types.InternalCodec<any>
71
74
  end,
72
75
 
73
76
  write = function(ch: Types.ChannelState, value: any): ()
74
- local c = ch.cursor
75
- if c + totalSize > ch.size then
77
+ local cursor = ch.cursor
78
+ if cursor + totalSize > ch.size then
76
79
  alloc(ch, totalSize)
77
80
  end
78
81
  local b = ch.buff
79
82
  for i = 1, count do
80
- directWrites[i](b, c + offsets[i], value[i])
83
+ directWrites[i](b, cursor + offsets[i], value[i])
81
84
  end
82
- ch.cursor = c + totalSize
85
+ ch.cursor = cursor + totalSize
83
86
  end,
84
87
 
85
88
  read = function(src: buffer, pos: number, _refs: { Instance }?): (any, number)
@@ -111,6 +114,9 @@ function Tuple.define(...: Types.InternalCodec<any>): Types.InternalCodec<any>
111
114
  local total = 0
112
115
  for i = 1, count do
113
116
  local v, consumed = codecs[i].read(src, pos + total, refs)
117
+ if consumed == 0 then
118
+ Log.error(`element codec consumed 0 bytes at index {i}`)
119
+ end
114
120
  result[i] = v
115
121
  total += consumed
116
122
  end
@@ -1,21 +1,23 @@
1
1
  --!strict
2
- --!optimize 2
2
+ --!native
3
3
  -- Raw buffer codec with dense-prefix-varint length prefix.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
  local Varint = require(script.Parent.Parent.primitive.Varint)
8
9
 
9
10
  -- Private ----------------------------------------------------------------
10
11
 
11
12
  local alloc = Base.alloc
12
- local INLINE = Varint.INLINE_MAX
13
+ local INLINE_MAX = Varint.INLINE_MAX
13
14
 
14
15
  local function writeBuffer(ch: Types.ChannelState, value: buffer): ()
15
16
  local len = buffer.len(value)
16
17
  local cursor = ch.cursor
17
18
 
18
- if len <= INLINE then
19
+ -- Single-byte length fast path skips the full varint encode.
20
+ if len <= INLINE_MAX then
19
21
  if cursor + 1 + len > ch.size then
20
22
  alloc(ch, 1 + len)
21
23
  end
@@ -45,7 +47,7 @@ local function readBuffer(src: buffer, pos: number, _refs: { Instance }?): (buff
45
47
 
46
48
  local dataStart = pos + lenBytes
47
49
  if dataStart + len > buffer.len(src) then
48
- error("[Lync] Buffer.read: length exceeds remaining buffer")
50
+ Log.error(`length {len}B exceeds remaining buffer`)
49
51
  end
50
52
 
51
53
  local out = buffer.create(len)