@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,98 +1,81 @@
1
1
  --!strict
2
2
  --!native
3
- -- Array and deltaArray codecs. Bool elements are bit-packed.
3
+ -- array() and deltaArray(). Bool elements are bit-packed.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
- local Channel = require(script.Parent.Parent.Parent.internal.Channel)
7
- local Shared = require(script.Parent.Shared)
6
+ local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
7
+ local Log = require(script.Parent.Parent.Parent.util.Log)
8
+ local Shared = require(script.Parent.Parent.composite.Shared)
8
9
  local Types = require(script.Parent.Parent.Parent.Types)
9
10
  local Varint = require(script.Parent.Parent.primitive.Varint)
10
11
 
12
+ -- Constants --------------------------------------------------------------
13
+
14
+ -- deltaArray wire flags. Distinct from Shared's struct/map flag spaces.
15
+ local DA_UNCHANGED = 0
16
+ local DA_FULL = 1
17
+ local DA_PATCH = 2
18
+
11
19
  -- Private ----------------------------------------------------------------
12
20
 
13
21
  local alloc = Base.alloc
14
- local writeByte = Channel.writeByte
15
- local writeu8 = buffer.writeu8
16
- local readu8 = buffer.readu8
17
- local band = bit32.band
18
- local bor = bit32.bor
19
- local lshift = bit32.lshift
20
-
22
+ local ensure = Base.ensure
23
+ local allocDeltaId = Shared.allocDeltaId
21
24
  local acquireScratch = Shared.acquireScratch
22
25
  local releaseScratch = Shared.releaseScratch
23
26
  local rangeEqual = Shared.rangeEqual
24
- local snapshot = Shared.snapshot
25
- local allocDeltaId = Shared.allocDeltaId
26
- local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
27
- local DELTA_FULL = Shared.DELTA_FULL
27
+ local copyToBuf = Shared.copyToBuf
28
+ local containsDelta = Shared.containsDelta
29
+ local enforceMaxCount = Shared.enforceMaxCount
30
+ local boolBytes = Shared.boolBytes
31
+ local packBoolArray = Shared.packBoolArray
32
+ local unpackBoolArray = Shared.unpackBoolArray
33
+ local varintRead = Varint.read
34
+ local varintWrite = Varint.write
35
+ local varintLength = Varint.length
36
+ local writeu8 = buffer.writeu8
37
+ local readu8 = buffer.readu8
38
+ local bufCopy = buffer.copy
39
+ local bufLen = buffer.len
40
+
41
+ type DeltaArrayEntry = { eBuf: buffer, eLen: number }
28
42
 
43
+ -- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
29
44
  local function makeBoolArray(
30
45
  element: Types.InternalCodec<any>,
31
46
  maxCount: number?
32
47
  ): Types.InternalCodec<any>
33
- return table.freeze({
48
+ return {
34
49
  _isArray = true,
35
50
  _element = element,
36
51
  _maxCount = maxCount,
37
52
 
38
53
  write = function(ch: Types.ChannelState, value: { boolean }): ()
39
54
  local len = #value
40
- Varint.write(ch, len)
41
- if len == 0 then
42
- return
43
- end
44
-
45
- local byteCount = (len + 7) // 8
46
- local cursor = ch.cursor
47
- if cursor + byteCount > ch.size then
48
- alloc(ch, byteCount)
49
- end
50
- local b = ch.buff
51
-
52
- local idx = 1
53
- for byteIdx = 0, byteCount - 1 do
54
- local packed = 0
55
- local remaining = len - idx + 1
56
- local limit = if remaining >= 8 then 8 else remaining
57
- for bit = 0, limit - 1 do
58
- if value[idx] then
59
- packed = bor(packed, lshift(1, bit))
60
- end
61
- idx += 1
62
- end
63
- writeu8(b, cursor + byteIdx, packed)
64
- end
65
- ch.cursor = cursor + byteCount
55
+ varintWrite(ch, len)
56
+ packBoolArray(ch, value, len)
66
57
  end,
67
58
 
68
59
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
69
- local len, lenBytes = Varint.read(src, pos)
70
- if maxCount and len > maxCount then
71
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
72
- end
60
+ local len, lenBytes = varintRead(src, pos)
61
+ enforceMaxCount(len, maxCount)
73
62
  if len == 0 then
74
63
  return {}, lenBytes
75
64
  end
76
65
 
77
- local byteCount = (len + 7) // 8
66
+ local byteCount = boolBytes(len)
78
67
  local dataStart = pos + lenBytes
79
- local result = table.create(len)
80
-
81
- local idx = 1
82
- for byteIdx = 0, byteCount - 1 do
83
- local byte = readu8(src, dataStart + byteIdx)
84
- local remaining = len - idx + 1
85
- local limit = if remaining >= 8 then 8 else remaining
86
- for bit = 0, limit - 1 do
87
- result[idx] = band(byte, lshift(1, bit)) ~= 0
88
- idx += 1
89
- end
68
+ if dataStart + byteCount > bufLen(src) then
69
+ Log.error(`payload {byteCount}B exceeds remaining buffer`)
90
70
  end
71
+ local result = table.create(len, false)
72
+ unpackBoolArray(src, dataStart, len, result)
91
73
  return result, lenBytes + byteCount
92
74
  end,
93
- })
75
+ }
94
76
  end
95
77
 
78
+ -- Fixed-size element fast path: single alloc + tight loop calling _direct fns.
96
79
  local function makeDirectArray(
97
80
  element: Types.InternalCodec<any>,
98
81
  elemSize: number,
@@ -100,14 +83,14 @@ local function makeDirectArray(
100
83
  elemDR: (b: buffer, off: number) -> any,
101
84
  maxCount: number?
102
85
  ): Types.InternalCodec<any>
103
- return table.freeze({
86
+ return {
104
87
  _isArray = true,
105
88
  _element = element,
106
89
  _maxCount = maxCount,
107
90
 
108
91
  write = function(ch: Types.ChannelState, value: { any }): ()
109
92
  local len = #value
110
- Varint.write(ch, len)
93
+ varintWrite(ch, len)
111
94
  if len == 0 then
112
95
  return
113
96
  end
@@ -118,7 +101,6 @@ local function makeDirectArray(
118
101
  alloc(ch, payloadBytes)
119
102
  end
120
103
  local b = ch.buff
121
-
122
104
  local off = cursor
123
105
  for i = 1, len do
124
106
  elemDW(b, off, value[i])
@@ -128,20 +110,16 @@ local function makeDirectArray(
128
110
  end,
129
111
 
130
112
  read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
131
- local len, lenBytes = Varint.read(src, pos)
132
- if maxCount and len > maxCount then
133
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
134
- end
113
+ local len, lenBytes = varintRead(src, pos)
114
+ enforceMaxCount(len, maxCount)
135
115
  if len == 0 then
136
116
  return {}, lenBytes
137
117
  end
138
118
 
139
119
  local payloadBytes = len * elemSize
140
120
  local dataStart = pos + lenBytes
141
- local remaining = buffer.len(src) - dataStart
142
- if payloadBytes > remaining then
143
- len = remaining // elemSize
144
- payloadBytes = len * elemSize
121
+ if payloadBytes > bufLen(src) - dataStart then
122
+ Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
145
123
  end
146
124
 
147
125
  local result = table.create(len)
@@ -152,31 +130,30 @@ local function makeDirectArray(
152
130
  end
153
131
  return result, lenBytes + payloadBytes
154
132
  end,
155
- })
133
+ }
156
134
  end
157
135
 
136
+ -- Generic fallback: variable-size or composite element codec.
158
137
  local function makeGenericArray(
159
138
  element: Types.InternalCodec<any>,
160
139
  maxCount: number?
161
140
  ): Types.InternalCodec<any>
162
- return table.freeze({
141
+ return {
163
142
  _isArray = true,
164
143
  _element = element,
165
144
  _maxCount = maxCount,
166
145
 
167
146
  write = function(ch: Types.ChannelState, value: { any }): ()
168
147
  local len = #value
169
- Varint.write(ch, len)
148
+ varintWrite(ch, len)
170
149
  for i = 1, len do
171
150
  element.write(ch, value[i])
172
151
  end
173
152
  end,
174
153
 
175
154
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
176
- local len, lenBytes = Varint.read(src, pos)
177
- if maxCount and len > maxCount then
178
- error(`[Lync] Array.read: count {len} exceeds max {maxCount}`)
179
- end
155
+ local len, lenBytes = varintRead(src, pos)
156
+ enforceMaxCount(len, maxCount)
180
157
  if len == 0 then
181
158
  return {}, lenBytes
182
159
  end
@@ -186,14 +163,14 @@ local function makeGenericArray(
186
163
  for i = 1, len do
187
164
  local v, consumed = element.read(src, pos + total, refs)
188
165
  if consumed == 0 then
189
- error(`[Lync] Array.read: element codec consumed 0 bytes at index {i}`)
166
+ Log.error(`element codec consumed 0 bytes at index {i}`)
190
167
  end
191
168
  result[i] = v
192
169
  total += consumed
193
170
  end
194
171
  return result, total
195
172
  end,
196
- })
173
+ }
197
174
  end
198
175
 
199
176
  -- Public -----------------------------------------------------------------
@@ -201,75 +178,279 @@ end
201
178
  local Array = {}
202
179
 
203
180
  function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Types.InternalCodec<any>
181
+ local codec: Types.InternalCodec<any>
204
182
  if element._isBool then
205
- return makeBoolArray(element, maxCount)
183
+ codec = makeBoolArray(element, maxCount)
184
+ else
185
+ local elemSize = element._size
186
+ local elemDW = element._directWrite
187
+ local elemDR = element._directRead
188
+ if elemSize and elemDW and elemDR then
189
+ codec = makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
190
+ else
191
+ codec = makeGenericArray(element, maxCount)
192
+ end
206
193
  end
207
-
208
- local elemSize = element._size
209
- local elemDW = element._directWrite
210
- local elemDR = element._directRead
211
- if elemSize and elemDW and elemDR then
212
- return makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
194
+ if element._hasUnknown then
195
+ codec._hasUnknown = true
213
196
  end
214
-
215
- return makeGenericArray(element, maxCount)
197
+ if containsDelta(element) then
198
+ codec._hasDelta = true
199
+ end
200
+ return table.freeze(codec)
216
201
  end
217
202
 
203
+ --[[
204
+ Per-index deltaArray. Wire:
205
+ [0] UNCHANGED (1 byte)
206
+ [1] FULL <count varint> <element bytes ...>
207
+ [2] PATCH <newLen varint> <changeCount varint>
208
+ <(idx varint, element bytes) ...>
209
+
210
+ Length shrinkage is implicit in newLen — entries past newLen drop from
211
+ the cached map. Length growth: appended indices show up as changes.
212
+ Reordered lists tend to mark every index changed; the writer falls back
213
+ to FULL when changeCount * 2 >= newLen since PATCH index varints eat
214
+ any savings past that ratio.
215
+ ]]
218
216
  function Array.deltaArray(
219
217
  element: Types.InternalCodec<any>,
220
218
  maxCount: number?
221
219
  ): Types.InternalCodec<any>
220
+ -- Inner delta would chain-diff across elements within a frame, not across frames.
221
+ if containsDelta(element) then
222
+ Log.error("deltaArray element cannot contain delta state")
223
+ end
224
+
222
225
  local deltaId = allocDeltaId()
223
- local innerArray = Array.array(element, maxCount)
226
+ local inner = Array.array(element, maxCount)
227
+
228
+ --[[
229
+ Encode each element into `scratch` and capture per-index byte ranges.
230
+ Returns the array length plus parallel arrays of offset/length so the
231
+ diff path can compare and emit individual entries without re-encoding.
232
+ ]]
233
+ local function encodePerIdx(
234
+ scratch: Types.ChannelState,
235
+ value: { any }
236
+ ): (number, { number }, { number })
237
+ local len = #value
238
+ enforceMaxCount(len, maxCount)
239
+ local off = table.create(len)
240
+ local elen = table.create(len)
241
+ for i = 1, len do
242
+ local before = scratch.cursor
243
+ element.write(scratch, value[i])
244
+ off[i] = before
245
+ elen[i] = scratch.cursor - before
246
+ end
247
+ return len, off, elen
248
+ end
224
249
 
225
250
  return table.freeze({
226
251
  _isDelta = true,
252
+ _isArray = true,
253
+ _element = element,
254
+ _maxCount = maxCount,
227
255
 
228
256
  write = function(ch: Types.ChannelState, value: { any }): ()
229
- local cache = ch.deltas[deltaId]
230
-
231
- if not cache then
232
- writeByte(ch, DELTA_FULL)
257
+ if typeof(value) ~= "table" then
258
+ Log.error(`expected table, got {typeof(value)}`)
259
+ end
233
260
 
234
- local scratch = acquireScratch()
235
- innerArray.write(scratch, value)
236
- local snapLen = scratch.cursor
261
+ local cache = ch.deltas[deltaId] :: any
262
+ local scratch = acquireScratch()
263
+ local len, off, elen = encodePerIdx(scratch, value)
264
+ local scratchBuf = scratch.buff
237
265
 
238
- ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
239
- innerArray.write(ch, value)
266
+ -- First frame: emit FULL using the per-index scratch bytes.
267
+ if not cache then
268
+ ensure(ch, 1)
269
+ writeu8(ch.buff, ch.cursor, DA_FULL)
270
+ ch.cursor += 1
271
+ varintWrite(ch, len)
272
+ local payloadLen = scratch.cursor
273
+ ensure(ch, payloadLen)
274
+ bufCopy(ch.buff, ch.cursor, scratchBuf, 0, payloadLen)
275
+ ch.cursor += payloadLen
276
+
277
+ local perIdx: { [number]: DeltaArrayEntry } = {}
278
+ for i = 1, len do
279
+ local n = elen[i]
280
+ perIdx[i] = { eBuf = copyToBuf(nil, scratchBuf, off[i], n), eLen = n }
281
+ end
282
+ ch.deltas[deltaId] = { perIdx = perIdx, length = len } :: any
240
283
  releaseScratch()
241
284
  return
242
285
  end
243
286
 
244
- local scratch = acquireScratch()
245
- innerArray.write(scratch, value)
246
- local scratchLen = scratch.cursor
247
-
248
- if
249
- scratchLen == cache.len
250
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
251
- then
252
- writeByte(ch, DELTA_UNCHANGED)
287
+ local cachedPerIdx = cache.perIdx :: { [number]: DeltaArrayEntry }
288
+ local cachedLen = cache.length :: number
289
+
290
+ -- Find changed indices in the overlap; appended indices count as changed.
291
+ local changed: { number } = {}
292
+ local changedCount = 0
293
+ local overlap = if len < cachedLen then len else cachedLen
294
+ for i = 1, overlap do
295
+ local entry = cachedPerIdx[i]
296
+ local n = elen[i]
297
+ if
298
+ not entry
299
+ or n ~= entry.eLen
300
+ or not rangeEqual(scratchBuf, off[i], entry.eBuf, 0, n)
301
+ then
302
+ changedCount += 1
303
+ changed[changedCount] = i
304
+ end
305
+ end
306
+ for i = overlap + 1, len do
307
+ changedCount += 1
308
+ changed[changedCount] = i
309
+ end
310
+
311
+ if changedCount == 0 and len == cachedLen then
312
+ ensure(ch, 1)
313
+ writeu8(ch.buff, ch.cursor, DA_UNCHANGED)
314
+ ch.cursor += 1
253
315
  releaseScratch()
254
316
  return
255
317
  end
256
318
 
257
- writeByte(ch, DELTA_FULL)
258
- innerArray.write(ch, value)
319
+ -- Pick FULL vs PATCH on actual byte cost. Both share `1 + varintLength(len)`
320
+ -- so it cancels: compare PATCH's index-header overhead + changed payload
321
+ -- against FULL's full payload.
322
+ local fullPayload = scratch.cursor
323
+ local patchPayload = varintLength(changedCount)
324
+ for i = 1, changedCount do
325
+ local idx = changed[i]
326
+ patchPayload += varintLength(idx) + elen[idx]
327
+ end
328
+
329
+ if patchPayload < fullPayload then
330
+ ensure(ch, 1)
331
+ writeu8(ch.buff, ch.cursor, DA_PATCH)
332
+ ch.cursor += 1
333
+ varintWrite(ch, len)
334
+ varintWrite(ch, changedCount)
335
+ for i = 1, changedCount do
336
+ local idx = changed[i]
337
+ varintWrite(ch, idx)
338
+ local n = elen[idx]
339
+ ensure(ch, n)
340
+ bufCopy(ch.buff, ch.cursor, scratchBuf, off[idx], n)
341
+ ch.cursor += n
342
+ end
343
+ else
344
+ ensure(ch, 1)
345
+ writeu8(ch.buff, ch.cursor, DA_FULL)
346
+ ch.cursor += 1
347
+ varintWrite(ch, len)
348
+ ensure(ch, fullPayload)
349
+ bufCopy(ch.buff, ch.cursor, scratchBuf, 0, fullPayload)
350
+ ch.cursor += fullPayload
351
+ end
352
+
353
+ -- Update cache: refresh changed (and appended), drop truncated.
354
+ for i = 1, changedCount do
355
+ local idx = changed[i]
356
+ local n = elen[idx]
357
+ local entry = cachedPerIdx[idx]
358
+ if entry then
359
+ entry.eBuf = copyToBuf(entry.eBuf, scratchBuf, off[idx], n)
360
+ entry.eLen = n
361
+ else
362
+ cachedPerIdx[idx] = { eBuf = copyToBuf(nil, scratchBuf, off[idx], n), eLen = n }
363
+ end
364
+ end
365
+ for i = len + 1, cachedLen do
366
+ cachedPerIdx[i] = nil
367
+ end
368
+ cache.length = len
259
369
 
260
- cache.raw = snapshot(scratch.buff, scratchLen)
261
- cache.len = scratchLen
262
370
  releaseScratch()
263
371
  end,
264
372
 
265
373
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
266
- if readu8(src, pos) == DELTA_UNCHANGED then
267
- return {}, 1
374
+ if pos >= bufLen(src) then
375
+ Log.error("truncated deltaArray header")
376
+ end
377
+ local flag = readu8(src, pos)
378
+
379
+ if flag == DA_UNCHANGED then
380
+ local cached = Baseline.getCache(deltaId)
381
+ if cached == nil then
382
+ Log.error("UNCHANGED flag before any FULL frame; baseline missing")
383
+ end
384
+ return cached, 1
385
+ end
386
+
387
+ if flag == DA_FULL then
388
+ local value, consumed = inner.read(src, pos + 1, refs)
389
+ Baseline.setCache(deltaId, value)
390
+ return value, 1 + consumed
391
+ end
392
+
393
+ if flag == DA_PATCH then
394
+ local cached = Baseline.getCache(deltaId)
395
+ if cached == nil then
396
+ Log.error("deltaArray PATCH before any FULL frame; baseline missing")
397
+ end
398
+ --[[
399
+ Capture the cached length BEFORE clone+mutate; #result
400
+ becomes unreliable once we set entries at appended indices
401
+ past the original boundary.
402
+ ]]
403
+ local cachedLen = #cached
404
+ local result: { any } = table.clone(cached)
405
+ local total = 1
406
+
407
+ local newLen, lLen = varintRead(src, pos + total)
408
+ if lLen == 0 then
409
+ Log.error("truncated deltaArray length")
410
+ end
411
+ enforceMaxCount(newLen, maxCount, "newLen")
412
+ total += lLen
413
+
414
+ local changeCount, cLen = varintRead(src, pos + total)
415
+ if cLen == 0 then
416
+ Log.error("truncated deltaArray change count")
417
+ end
418
+ -- changeCount is also bounded by newLen: every index appears at most once.
419
+ if changeCount > newLen then
420
+ Log.error(`deltaArray changeCount {changeCount} exceeds newLen {newLen}`)
421
+ end
422
+ total += cLen
423
+
424
+ for _ = 1, changeCount do
425
+ local idx, iLen = varintRead(src, pos + total)
426
+ if iLen == 0 then
427
+ Log.error("truncated deltaArray index")
428
+ end
429
+ -- Reject out-of-range idx so the wire can't poison the cache with
430
+ -- arbitrary keys that survive truncation.
431
+ if idx < 1 or idx > newLen then
432
+ Log.error(`deltaArray index {idx} out of range [1, {newLen}]`)
433
+ end
434
+ total += iLen
435
+ local v, vc = element.read(src, pos + total, refs)
436
+ if vc == 0 then
437
+ Log.error("truncated deltaArray element")
438
+ end
439
+ total += vc
440
+ result[idx] = v
441
+ end
442
+
443
+ for i = newLen + 1, cachedLen do
444
+ result[i] = nil
445
+ end
446
+
447
+ Baseline.setCache(deltaId, result)
448
+ return result, total
268
449
  end
269
- local value, consumed = innerArray.read(src, pos + 1, refs)
270
- return value, 1 + consumed
450
+
451
+ Log.error(`invalid deltaArray flag {flag}`)
271
452
  end,
272
- })
453
+ }) :: Types.InternalCodec<any>
273
454
  end
274
455
 
275
456
  return table.freeze(Array)