@axpecter/lync 2.2.1 → 2.3.2

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