@axpecter/lync 1.3.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 (52) hide show
  1. package/README.md +300 -0
  2. package/package.json +38 -0
  3. package/src/Types.luau +63 -0
  4. package/src/api/Group.luau +126 -0
  5. package/src/api/Namespace.luau +226 -0
  6. package/src/api/Packet.luau +147 -0
  7. package/src/api/Query.luau +295 -0
  8. package/src/api/Signal.luau +224 -0
  9. package/src/codec/Base.luau +49 -0
  10. package/src/codec/composite/Array.luau +275 -0
  11. package/src/codec/composite/Map.luau +395 -0
  12. package/src/codec/composite/Optional.luau +47 -0
  13. package/src/codec/composite/Shared.luau +151 -0
  14. package/src/codec/composite/Struct.luau +440 -0
  15. package/src/codec/composite/Tagged.luau +222 -0
  16. package/src/codec/composite/Tuple.luau +143 -0
  17. package/src/codec/datatype/Buffer.luau +44 -0
  18. package/src/codec/datatype/CFrame.luau +51 -0
  19. package/src/codec/datatype/Color.luau +22 -0
  20. package/src/codec/datatype/Instance.luau +48 -0
  21. package/src/codec/datatype/IntVector.luau +25 -0
  22. package/src/codec/datatype/NumberRange.luau +14 -0
  23. package/src/codec/datatype/Ray.luau +27 -0
  24. package/src/codec/datatype/Rect.luau +21 -0
  25. package/src/codec/datatype/Region.luau +58 -0
  26. package/src/codec/datatype/Sequence.luau +129 -0
  27. package/src/codec/datatype/String.luau +87 -0
  28. package/src/codec/datatype/UDim.luau +27 -0
  29. package/src/codec/datatype/Vector.luau +25 -0
  30. package/src/codec/meta/Auto.luau +353 -0
  31. package/src/codec/meta/Bitfield.luau +191 -0
  32. package/src/codec/meta/Custom.luau +27 -0
  33. package/src/codec/meta/Enum.luau +80 -0
  34. package/src/codec/meta/Nothing.luau +9 -0
  35. package/src/codec/meta/Quantized.luau +170 -0
  36. package/src/codec/meta/Unknown.luau +35 -0
  37. package/src/codec/primitive/Bool.luau +30 -0
  38. package/src/codec/primitive/Float16.luau +111 -0
  39. package/src/codec/primitive/Number.luau +48 -0
  40. package/src/codec/primitive/Varint.luau +76 -0
  41. package/src/index.d.ts +279 -0
  42. package/src/init.luau +161 -0
  43. package/src/internal/Baseline.luau +41 -0
  44. package/src/internal/Channel.luau +235 -0
  45. package/src/internal/Middleware.luau +109 -0
  46. package/src/internal/Pool.luau +68 -0
  47. package/src/internal/Registry.luau +146 -0
  48. package/src/transport/Bridge.luau +66 -0
  49. package/src/transport/Client.luau +151 -0
  50. package/src/transport/Gate.luau +222 -0
  51. package/src/transport/Reader.luau +175 -0
  52. package/src/transport/Server.luau +364 -0
@@ -0,0 +1,440 @@
1
+ --!strict
2
+ --!native
3
+ -- struct and deltaStruct codecs.
4
+
5
+ local Baseline = require (script.Parent.Parent.Parent.internal.Baseline)
6
+ local Channel = require (script.Parent.Parent.Parent.internal.Channel)
7
+ local alloc = Channel.alloc
8
+ local Shared = require (script.Parent.Shared)
9
+ local Types = require (script.Parent.Parent.Parent.Types)
10
+
11
+ type ChannelState = Types.ChannelState
12
+ type Codec<T> = Types.Codec<T>
13
+
14
+ local extractFields = Shared.extractFields
15
+ local packBools = Shared.packBools
16
+ local unpackBools = Shared.unpackBools
17
+ local rangeEqual = Shared.rangeEqual
18
+ local acquireScratch = Shared.acquireScratch
19
+ local releaseScratch = Shared.releaseScratch
20
+
21
+ local FLAG_DELTA = Shared.FLAG_DELTA
22
+ local FLAG_FULL = Shared.FLAG_FULL
23
+ local FLAG_UNCHANGED = Shared.FLAG_UNCHANGED
24
+
25
+ local band = bit32.band
26
+ local bor = bit32.bor
27
+ local lshift = bit32.lshift
28
+ local ceil = math.ceil
29
+ local min = math.min
30
+
31
+ type DeltaCache = {
32
+ raw: buffer,
33
+ bounds: { number },
34
+ total: number,
35
+ }
36
+
37
+ -- Public -----------------------------------------------------------------
38
+
39
+ local Struct = {}
40
+
41
+ function Struct.struct (schema: { [string]: Codec<any> }): Codec<any>
42
+ local dataKeys, dataCodecs, boolKeys = extractFields (schema)
43
+
44
+ local dataCount = #dataKeys
45
+ local boolCount = #boolKeys
46
+ local boolByteCount = ceil (boolCount / 8)
47
+ local hasBools = boolCount > 0
48
+
49
+ local canDirect = true
50
+ local fixedSize = boolByteCount
51
+ local isAllFixed = true
52
+
53
+ local directWrites = table.create (dataCount) :: { any }
54
+ local directReads = table.create (dataCount) :: { any }
55
+ local fieldSizes = table.create (dataCount) :: { number }
56
+
57
+ for i = 1, dataCount do
58
+ local codec = dataCodecs[i] :: any
59
+ local size = codec._size
60
+ local dw = codec._directWrite
61
+ local dr = codec._directRead
62
+
63
+ if size then
64
+ fixedSize += size
65
+ fieldSizes[i] = size
66
+ else
67
+ isAllFixed = false
68
+ canDirect = false
69
+ end
70
+
71
+ if not (dw and dr and size) then
72
+ canDirect = false
73
+ else
74
+ directWrites[i] = dw
75
+ directReads[i] = dr
76
+ end
77
+ end
78
+
79
+ if canDirect and isAllFixed then
80
+ table.freeze (directWrites)
81
+ table.freeze (directReads)
82
+ table.freeze (fieldSizes)
83
+
84
+ local function directWriteBody (b: buffer, c: number, value: any): ()
85
+ for i = 1, dataCount do
86
+ directWrites[i] (b, c, value[dataKeys[i]])
87
+ c += fieldSizes[i]
88
+ end
89
+ if hasBools then
90
+ for byteIdx = 0, boolByteCount - 1 do
91
+ local packed = 0
92
+ local base = byteIdx * 8
93
+ local limit = min (8, boolCount - base)
94
+ for bit = 0, limit - 1 do
95
+ if value[boolKeys[base + bit + 1]] then
96
+ packed = bor (packed, lshift (1, bit))
97
+ end
98
+ end
99
+ buffer.writeu8 (b, c + byteIdx, packed)
100
+ end
101
+ end
102
+ end
103
+
104
+ local function directReadBody (b: buffer, pos: number): any
105
+ local result = {}
106
+ local c = pos
107
+ for i = 1, dataCount do
108
+ result[dataKeys[i]] = directReads[i] (b, c)
109
+ c += fieldSizes[i]
110
+ end
111
+ if hasBools then
112
+ for byteIdx = 0, boolByteCount - 1 do
113
+ local byte = buffer.readu8 (b, c + byteIdx)
114
+ local base = byteIdx * 8
115
+ local limit = min (8, boolCount - base)
116
+ for bit = 0, limit - 1 do
117
+ result[boolKeys[base + bit + 1]] = band (byte, lshift (1, bit)) ~= 0
118
+ end
119
+ end
120
+ end
121
+ return result
122
+ end
123
+
124
+ return table.freeze ({
125
+ _size = fixedSize,
126
+ _directWrite = directWriteBody,
127
+ _directRead = directReadBody,
128
+
129
+ write = function (ch: ChannelState, value: any): ()
130
+ local c = ch.cursor
131
+ if c + fixedSize > ch.size then
132
+ alloc (ch, fixedSize)
133
+ end
134
+ directWriteBody (ch.buff, c, value)
135
+ ch.cursor = c + fixedSize
136
+ end,
137
+ read = function (src: buffer, pos: number, _refs: { Instance }?): (any, number)
138
+ return directReadBody (src, pos), fixedSize
139
+ end,
140
+ })
141
+ end
142
+
143
+ local writeFns = table.create (dataCount) :: { any }
144
+ local readFns = table.create (dataCount) :: { any }
145
+ for i = 1, dataCount do
146
+ writeFns[i] = dataCodecs[i].write
147
+ readFns[i] = dataCodecs[i].read
148
+ end
149
+ table.freeze (writeFns)
150
+ table.freeze (readFns)
151
+
152
+ return table.freeze ({
153
+ _size = if isAllFixed then fixedSize else nil,
154
+
155
+ write = function (ch: ChannelState, value: any): ()
156
+ if isAllFixed then
157
+ alloc (ch, fixedSize)
158
+ end
159
+
160
+ for i = 1, dataCount do
161
+ writeFns[i] (ch, value[dataKeys[i]])
162
+ end
163
+
164
+ if hasBools then
165
+ packBools (ch, boolKeys, boolCount, value)
166
+ end
167
+ end,
168
+ read = function (src: buffer, pos: number, refs: { Instance }?): (any, number)
169
+ local result = {}
170
+ local absPos = pos
171
+
172
+ for i = 1, dataCount do
173
+ local value, n = readFns[i] (src, absPos, refs)
174
+ result[dataKeys[i]] = value
175
+ absPos += n
176
+ end
177
+
178
+ if hasBools then
179
+ absPos += unpackBools (src, absPos, boolKeys, boolCount, result)
180
+ end
181
+
182
+ return result, absPos - pos
183
+ end,
184
+ })
185
+ end
186
+ function Struct.deltaStruct (schema: { [string]: Codec<any> }): Codec<any>
187
+ local dataKeys, dataCodecs, boolKeys = extractFields (schema)
188
+
189
+ local dataCount = #dataKeys
190
+ local boolCount = #boolKeys
191
+ local boolByteCount = ceil (boolCount / 8)
192
+ local segCount = dataCount + (if boolCount > 0 then 1 else 0)
193
+ local bitmaskBytes = ceil (segCount / 8)
194
+ local deltaId = Shared.allocDeltaId ()
195
+
196
+ -- Scratch bounds reused every frame (only cloned into cache)
197
+ local _scratchBounds = table.create (segCount + 1) :: { number }
198
+ _scratchBounds[1] = 0
199
+
200
+ local writeFns = table.create (dataCount) :: { any }
201
+ local readFns = table.create (dataCount) :: { any }
202
+ for i = 1, dataCount do
203
+ writeFns[i] = dataCodecs[i].write
204
+ readFns[i] = dataCodecs[i].read
205
+ end
206
+ table.freeze (writeFns)
207
+ table.freeze (readFns)
208
+
209
+ local scratchDirect = true
210
+ local scratchDirectWrites = table.create (dataCount) :: { any }
211
+ local scratchFieldSizes = table.create (dataCount) :: { number }
212
+ local scratchFieldOffsets = table.create (dataCount) :: { number }
213
+ local scratchDataTotal = 0
214
+
215
+ for i = 1, dataCount do
216
+ local codec = dataCodecs[i] :: any
217
+ local size = codec._size
218
+ local dw = codec._directWrite
219
+ if size and dw then
220
+ scratchDirectWrites[i] = dw
221
+ scratchFieldSizes[i] = size
222
+ scratchFieldOffsets[i] = scratchDataTotal
223
+ scratchDataTotal += size
224
+ else
225
+ scratchDirect = false
226
+ break
227
+ end
228
+ end
229
+
230
+ local scratchTotalFixed = scratchDataTotal + boolByteCount
231
+
232
+ -- Skip scratch alloc at runtime when the total fits in the initial
233
+ -- 1024-byte scratch buffer.
234
+ local scratchSkipAlloc = scratchDirect and scratchTotalFixed <= 1024
235
+
236
+ return table.freeze ({
237
+ _isDelta = true,
238
+
239
+ write = function (ch: ChannelState, value: any): ()
240
+ local cache = ch.deltas[deltaId] :: DeltaCache?
241
+ local scratch = acquireScratch ()
242
+
243
+ local bounds = _scratchBounds
244
+ bounds[1] = 0
245
+
246
+ if scratchDirect then
247
+ if not scratchSkipAlloc then
248
+ alloc (scratch, scratchTotalFixed)
249
+ end
250
+ local b = scratch.buff
251
+
252
+ for i = 1, dataCount do
253
+ scratchDirectWrites[i] (b, scratchFieldOffsets[i], value[dataKeys[i]])
254
+ bounds[i + 1] = scratchFieldOffsets[i] + scratchFieldSizes[i]
255
+ end
256
+ scratch.cursor = scratchDataTotal
257
+
258
+ if boolCount > 0 then
259
+ packBools (scratch, boolKeys, boolCount, value)
260
+ bounds[segCount + 1] = scratch.cursor
261
+ end
262
+ else
263
+ for i = 1, dataCount do
264
+ writeFns[i] (scratch, value[dataKeys[i]])
265
+ bounds[i + 1] = scratch.cursor
266
+ end
267
+
268
+ if boolCount > 0 then
269
+ packBools (scratch, boolKeys, boolCount, value)
270
+ bounds[segCount + 1] = scratch.cursor
271
+ end
272
+ end
273
+
274
+ local scratchTotal = scratch.cursor
275
+
276
+ -- FLAG_FULL: first frame, no cache exists yet
277
+ if not cache then
278
+ alloc (ch, 1 + scratchTotal)
279
+ buffer.writeu8 (ch.buff, ch.cursor, FLAG_FULL)
280
+ ch.cursor += 1
281
+ buffer.copy (ch.buff, ch.cursor, scratch.buff, 0, scratchTotal)
282
+ ch.cursor += scratchTotal
283
+
284
+ -- FLAG_UNCHANGED: entire buffer is byte-identical to cache
285
+ elseif
286
+ scratchTotal == cache.total
287
+ and rangeEqual (scratch.buff, 0, cache.raw, 0, scratchTotal)
288
+ then
289
+ alloc (ch, 1)
290
+ buffer.writeu8 (ch.buff, ch.cursor, FLAG_UNCHANGED)
291
+ ch.cursor += 1
292
+ releaseScratch ()
293
+ return
294
+
295
+ -- FLAG_DELTA: per-segment dirty mask + dirty payloads
296
+ elseif bitmaskBytes == 1 then
297
+ local mask = 0
298
+ local cacheRaw = cache.raw
299
+ local cacheBds = cache.bounds
300
+
301
+ for i = 1, segCount do
302
+ local newOff = bounds[i]
303
+ local newLen = bounds[i + 1] - newOff
304
+ local oldOff = cacheBds[i]
305
+ local oldLen = cacheBds[i + 1] - oldOff
306
+
307
+ if
308
+ newLen ~= oldLen
309
+ or not rangeEqual (scratch.buff, newOff, cacheRaw, oldOff, newLen)
310
+ then
311
+ mask = bor (mask, lshift (1, i - 1))
312
+ end
313
+ end
314
+
315
+ alloc (ch, 2)
316
+ buffer.writeu8 (ch.buff, ch.cursor, FLAG_DELTA)
317
+ buffer.writeu8 (ch.buff, ch.cursor + 1, mask)
318
+ ch.cursor += 2
319
+
320
+ for i = 1, segCount do
321
+ if band (mask, lshift (1, i - 1)) ~= 0 then
322
+ local newOff = bounds[i]
323
+ local newLen = bounds[i + 1] - newOff
324
+ alloc (ch, newLen)
325
+ buffer.copy (ch.buff, ch.cursor, scratch.buff, newOff, newLen)
326
+ ch.cursor += newLen
327
+ end
328
+ end
329
+ else
330
+ local maskBuf = table.create (bitmaskBytes, 0)
331
+ local cacheRaw = cache.raw
332
+ local cacheBds = cache.bounds
333
+
334
+ for i = 1, segCount do
335
+ local newOff = bounds[i]
336
+ local newLen = bounds[i + 1] - newOff
337
+ local oldOff = cacheBds[i]
338
+ local oldLen = cacheBds[i + 1] - oldOff
339
+
340
+ if
341
+ newLen ~= oldLen
342
+ or not rangeEqual (scratch.buff, newOff, cacheRaw, oldOff, newLen)
343
+ then
344
+ local bitIdx = i - 1
345
+ local byteIdx = bitIdx // 8 + 1
346
+ maskBuf[byteIdx] = bor (maskBuf[byteIdx], lshift (1, bitIdx % 8))
347
+ end
348
+ end
349
+
350
+ alloc (ch, 1 + bitmaskBytes)
351
+ buffer.writeu8 (ch.buff, ch.cursor, FLAG_DELTA)
352
+ ch.cursor += 1
353
+
354
+ local maskStart = ch.cursor
355
+ for byteIdx = 1, bitmaskBytes do
356
+ buffer.writeu8 (ch.buff, maskStart + byteIdx - 1, maskBuf[byteIdx])
357
+ end
358
+ ch.cursor = maskStart + bitmaskBytes
359
+
360
+ for i = 1, segCount do
361
+ local bitIdx = i - 1
362
+ local byteIdx = bitIdx // 8 + 1
363
+ if band (maskBuf[byteIdx], lshift (1, bitIdx % 8)) ~= 0 then
364
+ local newOff = bounds[i]
365
+ local newLen = bounds[i + 1] - newOff
366
+ alloc (ch, newLen)
367
+ buffer.copy (ch.buff, ch.cursor, scratch.buff, newOff, newLen)
368
+ ch.cursor += newLen
369
+ end
370
+ end
371
+ end
372
+
373
+ local rawBuf = buffer.create (scratchTotal)
374
+ buffer.copy (rawBuf, 0, scratch.buff, 0, scratchTotal)
375
+
376
+ local cacheBounds = table.clone (bounds)
377
+ table.freeze (cacheBounds)
378
+ ch.deltas[deltaId] = {
379
+ raw = rawBuf,
380
+ bounds = cacheBounds,
381
+ total = scratchTotal,
382
+ } :: DeltaCache
383
+
384
+ releaseScratch ()
385
+ end,
386
+
387
+ read = function (src: buffer, pos: number, refs: { Instance }?): (any, number)
388
+ local flag = buffer.readu8 (src, pos)
389
+ local absPos = pos + 1
390
+
391
+ if flag == FLAG_UNCHANGED then
392
+ local cache = Baseline.getCache (deltaId) :: any
393
+ return if cache then table.clone (cache) else {}, 1
394
+ end
395
+
396
+ if flag == FLAG_FULL then
397
+ local result = {}
398
+
399
+ for i = 1, dataCount do
400
+ local val, n = readFns[i] (src, absPos, refs)
401
+ result[dataKeys[i]] = val
402
+ absPos += n
403
+ end
404
+
405
+ if boolCount > 0 then
406
+ absPos += unpackBools (src, absPos, boolKeys, boolCount, result)
407
+ end
408
+
409
+ Baseline.setCache (deltaId, result)
410
+ return result, absPos - pos
411
+ end
412
+
413
+ local cache = Baseline.getCache (deltaId) :: any
414
+ local result = if cache then table.clone (cache) else {}
415
+
416
+ local maskStart = absPos
417
+ absPos += bitmaskBytes
418
+
419
+ for i = 1, segCount do
420
+ local bitIdx = i - 1
421
+ local byte = buffer.readu8 (src, maskStart + (bitIdx // 8))
422
+
423
+ if band (byte, lshift (1, bitIdx % 8)) ~= 0 then
424
+ if i <= dataCount then
425
+ local val, n = readFns[i] (src, absPos, refs)
426
+ result[dataKeys[i]] = val
427
+ absPos += n
428
+ else
429
+ absPos += unpackBools (src, absPos, boolKeys, boolCount, result)
430
+ end
431
+ end
432
+ end
433
+
434
+ Baseline.setCache (deltaId, result)
435
+ return result, absPos - pos
436
+ end,
437
+ })
438
+ end
439
+
440
+ return table.freeze (Struct)
@@ -0,0 +1,222 @@
1
+ --!strict
2
+ --!native
3
+ -- Discriminated union codec. u8 variant tag + variant payload.
4
+
5
+ local Channel = require (script.Parent.Parent.Parent.internal.Channel)
6
+ local alloc = Channel.alloc
7
+ local Types = require (script.Parent.Parent.Parent.Types)
8
+
9
+ type ChannelState = Types.ChannelState
10
+ type Codec<T> = Types.Codec<T>
11
+
12
+ -- Public -----------------------------------------------------------------
13
+
14
+ local Tagged = {}
15
+
16
+ -- Read injects tagField into the decoded table. Variant codecs must return mutable tables.
17
+ function Tagged.define (tagField: string, variants: { [string]: Codec<any> }): Codec<any>
18
+ if #tagField == 0 then
19
+ error ("[Lync] Tagged union tagField must not be empty")
20
+ end
21
+
22
+ local names = {} :: { string }
23
+ for name in variants do
24
+ if #name == 0 then
25
+ error ("[Lync] Tagged union variant name must not be empty")
26
+ end
27
+ table.insert (names, name)
28
+ end
29
+ table.sort (names)
30
+
31
+ local count = #names
32
+ if count == 0 then
33
+ error ("[Lync] Tagged union requires at least one variant")
34
+ end
35
+ if count > 256 then
36
+ error (`[Lync] Tagged union exceeds 256 variants: {count}`)
37
+ end
38
+
39
+ local nameToTag = {} :: { [string]: number }
40
+ local tagCodecs = table.create (count) :: { Codec<any> }
41
+ local tagNames = table.create (count) :: { string }
42
+ local tagSizes = table.create (count) :: { number? }
43
+
44
+ local allDirect = true
45
+ local uniformSize: number? = (variants[names[1]] :: any)._size
46
+ local directWrites = table.create (count) :: { any }
47
+ local directReads = table.create (count) :: { any }
48
+
49
+ for i, name in names do
50
+ local codec = variants[name] :: any
51
+ nameToTag[name] = i
52
+ tagCodecs[i] = codec
53
+ tagNames[i] = name
54
+
55
+ local size = codec._size
56
+ tagSizes[i] = size
57
+
58
+ if size ~= uniformSize then
59
+ uniformSize = nil
60
+ end
61
+
62
+ local dw = codec._directWrite
63
+ local dr = codec._directRead
64
+ if dw and dr and size then
65
+ directWrites[i] = dw
66
+ directReads[i] = dr
67
+ else
68
+ allDirect = false
69
+ end
70
+ end
71
+
72
+ table.freeze (nameToTag)
73
+ table.freeze (tagCodecs)
74
+ table.freeze (tagNames)
75
+ table.freeze (tagSizes)
76
+
77
+ local fixedSize: number? = if uniformSize then 1 + uniformSize else nil
78
+
79
+ if allDirect then
80
+ table.freeze (directWrites)
81
+ table.freeze (directReads)
82
+
83
+ -- If uniform size, expose _directWrite/_directRead for parent struct/array
84
+ if fixedSize then
85
+ local variantBytes = uniformSize :: number
86
+
87
+ return table.freeze ({
88
+ _size = fixedSize,
89
+ _directWrite = function (b: buffer, offset: number, value: any): ()
90
+ local tag = nameToTag[value[tagField]]
91
+ if not tag then
92
+ error (`[Lync] Unknown variant: {value[tagField]}`)
93
+ end
94
+ buffer.writeu8 (b, offset, tag - 1)
95
+ directWrites[tag] (b, offset + 1, value)
96
+ end,
97
+ _directRead = function (b: buffer, offset: number): any
98
+ local tag = buffer.readu8 (b, offset) + 1
99
+ local name = tagNames[tag]
100
+ if not name then
101
+ error (`[Lync] Unknown variant index: {tag - 1}`)
102
+ end
103
+ local data = directReads[tag] (b, offset + 1)
104
+ if type (data) == "table" then
105
+ data[tagField] = name
106
+ end
107
+ return data
108
+ end,
109
+ write = function (ch: ChannelState, value: any): ()
110
+ local tag = nameToTag[value[tagField]]
111
+ if not tag then
112
+ error (`[Lync] Unknown variant: {value[tagField]}`)
113
+ end
114
+ local c = ch.cursor
115
+ if c + (fixedSize :: number) > ch.size then
116
+ alloc (ch, fixedSize :: number)
117
+ end
118
+ local b = ch.buff
119
+ buffer.writeu8 (b, c, tag - 1)
120
+ directWrites[tag] (b, c + 1, value)
121
+ ch.cursor = c + (fixedSize :: number)
122
+ end,
123
+ read = function (src: buffer, pos: number, _refs: { Instance }?): (any, number)
124
+ local tag = buffer.readu8 (src, pos) + 1
125
+ local name = tagNames[tag]
126
+ if not name then
127
+ error (`[Lync] Unknown variant index: {tag - 1}`)
128
+ end
129
+ local data = directReads[tag] (src, pos + 1)
130
+ if type (data) == "table" then
131
+ data[tagField] = name
132
+ end
133
+ return data, fixedSize :: number
134
+ end,
135
+ })
136
+ end
137
+
138
+ -- Non-uniform sizes but all have direct: use direct dispatch internally
139
+ local fieldSizes = table.clone (tagSizes) :: { number }
140
+ table.freeze (fieldSizes)
141
+
142
+ return table.freeze ({
143
+ _size = nil,
144
+ write = function (ch: ChannelState, value: any): ()
145
+ local tag = nameToTag[value[tagField]]
146
+ if not tag then
147
+ error (`[Lync] Unknown variant: {value[tagField]}`)
148
+ end
149
+ local varSize = fieldSizes[tag]
150
+ local totalNeeded = 1 + varSize
151
+ local c = ch.cursor
152
+ if c + totalNeeded > ch.size then
153
+ alloc (ch, totalNeeded)
154
+ end
155
+ local b = ch.buff
156
+ buffer.writeu8 (b, c, tag - 1)
157
+ directWrites[tag] (b, c + 1, value)
158
+ ch.cursor = c + totalNeeded
159
+ end,
160
+ read = function (src: buffer, pos: number, _refs: { Instance }?): (any, number)
161
+ local tag = buffer.readu8 (src, pos) + 1
162
+ local name = tagNames[tag]
163
+ if not name then
164
+ error (`[Lync] Unknown variant index: {tag - 1}`)
165
+ end
166
+ local data = directReads[tag] (src, pos + 1)
167
+ if type (data) == "table" then
168
+ data[tagField] = name
169
+ end
170
+ return data, 1 + fieldSizes[tag]
171
+ end,
172
+ })
173
+ end
174
+
175
+ local writeFns = table.create (count) :: { any }
176
+ local readFns = table.create (count) :: { any }
177
+ for i = 1, count do
178
+ writeFns[i] = tagCodecs[i].write
179
+ readFns[i] = tagCodecs[i].read
180
+ end
181
+ table.freeze (writeFns)
182
+ table.freeze (readFns)
183
+
184
+ return table.freeze ({
185
+ _size = fixedSize,
186
+ write = function (ch: ChannelState, value: any): ()
187
+ local tag = nameToTag[value[tagField]]
188
+ if not tag then
189
+ error (`[Lync] Unknown variant: {value[tagField]}`)
190
+ end
191
+
192
+ local variantSize = tagSizes[tag]
193
+ if variantSize then
194
+ alloc (ch, 1 + variantSize)
195
+ else
196
+ alloc (ch, 1)
197
+ end
198
+
199
+ local c = ch.cursor
200
+ buffer.writeu8 (ch.buff, c, tag - 1)
201
+ ch.cursor = c + 1
202
+ writeFns[tag] (ch, value)
203
+ end,
204
+ read = function (src: buffer, pos: number, refs: { Instance }?): (any, number)
205
+ local tag = buffer.readu8 (src, pos) + 1
206
+ local name = tagNames[tag]
207
+ if not name then
208
+ error (`[Lync] Unknown variant index: {tag - 1}`)
209
+ end
210
+
211
+ local data, bytes = readFns[tag] (src, pos + 1, refs)
212
+
213
+ if type (data) == "table" then
214
+ data[tagField] = name
215
+ end
216
+
217
+ return data, 1 + bytes
218
+ end,
219
+ })
220
+ end
221
+
222
+ return table.freeze (Tagged)