@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,26 +1,97 @@
1
1
  --!strict
2
2
  --!native
3
- -- Map and deltaMap codecs.
3
+ -- map() and deltaMap(). Keys sorted at encode for stable wire bytes.
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
 
11
- -- Private ----------------------------------------------------------------
12
+ -- Constants --------------------------------------------------------------
12
13
 
13
- local alloc = Base.alloc
14
- local writeByte = Channel.writeByte
15
- local readu8 = buffer.readu8
14
+ -- deltaMap wire flags. Distinct from Shared's struct/array flag spaces.
15
+ local DM_UNCHANGED = 0
16
+ local DM_FULL = 1
17
+ local DM_DIFF = 2
18
+
19
+ -- State ------------------------------------------------------------------
20
+
21
+ --[[
22
+ Depth-stacked key scratch. A nested Map.write (Map<K, Map<K2, V>>) would
23
+ clobber a single shared array mid-iteration; the stack gives each frame
24
+ its own slot. Mirrors Shared.acquireScratch / releaseScratch.
25
+ ]]
26
+ local _keyStacks: { { any } } = {}
27
+ local _keyDepth = 0
28
+
29
+ -- Private ----------------------------------------------------------------
16
30
 
31
+ local ensure = Base.ensure
32
+ local allocDeltaId = Shared.allocDeltaId
17
33
  local acquireScratch = Shared.acquireScratch
18
34
  local releaseScratch = Shared.releaseScratch
19
35
  local rangeEqual = Shared.rangeEqual
20
- local snapshot = Shared.snapshot
21
- local allocDeltaId = Shared.allocDeltaId
22
- local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
23
- local DELTA_FULL = Shared.DELTA_FULL
36
+ local copyToBuf = Shared.copyToBuf
37
+ local containsDelta = Shared.containsDelta
38
+ local enforceMaxCount = Shared.enforceMaxCount
39
+ local writeu8 = buffer.writeu8
40
+ local readu8 = buffer.readu8
41
+ local bufCopy = buffer.copy
42
+ local bufLen = buffer.len
43
+ local varintRead = Varint.read
44
+ local varintWrite = Varint.write
45
+
46
+ type DeltaMapEntry = { kBuf: buffer, kLen: number, vBuf: buffer, vLen: number }
47
+
48
+ local function acquireKeys(): { any }
49
+ _keyDepth += 1
50
+ local keys = _keyStacks[_keyDepth]
51
+ if keys then
52
+ return keys
53
+ end
54
+ keys = {}
55
+ _keyStacks[_keyDepth] = keys
56
+ return keys
57
+ end
58
+
59
+ local function releaseKeys(): ()
60
+ _keyDepth -= 1
61
+ end
62
+
63
+ -- Module-level so the mixed-type sort path doesn't allocate a closure per write.
64
+ local function tostringLess(a: any, b: any): boolean
65
+ return tostring(a) < tostring(b)
66
+ end
67
+
68
+ local function sortKeys(keys: { any }, canSortDirect: boolean): ()
69
+ if canSortDirect then
70
+ table.sort(keys :: { any })
71
+ else
72
+ table.sort(keys, tostringLess)
73
+ end
74
+ end
75
+
76
+ --[[
77
+ Acquire a scratch keys array, populate it from `value`, nil any leftover
78
+ tail entries from a prior reuse, then sort. Returns the keys array and
79
+ the populated count. Caller MUST releaseKeys() after consuming the keys.
80
+ ]]
81
+ local function collectAndSortKeys(value: { [any]: any }, canSortDirect: boolean): ({ any }, number)
82
+ local keys = acquireKeys()
83
+ local count = 0
84
+ for k in value do
85
+ count += 1
86
+ keys[count] = k
87
+ end
88
+ local prevTail = #keys
89
+ for i = count + 1, prevTail do
90
+ keys[i] = nil
91
+ end
92
+ sortKeys(keys, canSortDirect)
93
+ return keys, count
94
+ end
24
95
 
25
96
  -- Public -----------------------------------------------------------------
26
97
 
@@ -33,117 +104,366 @@ function Map.map(
33
104
  ): Types.InternalCodec<any>
34
105
  local fixedKey = keyCodec._size
35
106
  local fixedValue = valueCodec._size
107
+ local hasUnknown = keyCodec._hasUnknown or valueCodec._hasUnknown
108
+ local hasDelta = containsDelta(keyCodec) or containsDelta(valueCodec)
36
109
 
37
- return table.freeze({
110
+ --[[
111
+ Numbers and strings sort by `<` directly; everything else (Instance,
112
+ vector, mixed-type via Auto) routes through tostring. Pinned at codec
113
+ creation so writes don't re-sniff the key type.
114
+ ]]
115
+ local typeCheck = keyCodec._typeCheck
116
+ local canSortDirect = typeCheck == "number" or typeCheck == "string"
117
+
118
+ local codec: Types.InternalCodec<any> = {
38
119
  _isMap = true,
39
120
  _keyCodec = keyCodec,
40
121
  _valueCodec = valueCodec,
41
122
  _maxCount = maxCount,
42
123
 
124
+ --[[
125
+ Sort keys for deterministic wire layout. Without this, two
126
+ structurally-equal maps yield different byte sequences across
127
+ frames, which breaks every downstream byte-equality cache (the
128
+ XOR baseline and deltaMap).
129
+ ]]
43
130
  write = function(ch: Types.ChannelState, value: { [any]: any }): ()
44
- local count = 0
45
- for _ in value do
46
- count += 1
131
+ if typeof(value) ~= "table" then
132
+ Log.error(`expected table, got {typeof(value)}`)
47
133
  end
48
134
 
49
- Varint.write(ch, count)
135
+ local keys, count = collectAndSortKeys(value, canSortDirect)
136
+
137
+ varintWrite(ch, count)
50
138
  if count == 0 then
139
+ releaseKeys()
51
140
  return
52
141
  end
53
142
 
143
+ -- Single alloc when both halves are fixed-size.
54
144
  if fixedKey and fixedValue then
55
- local entrySize = fixedKey + fixedValue
56
- local cursor = ch.cursor
57
- if cursor + count * entrySize > ch.size then
58
- alloc(ch, count * entrySize)
59
- end
145
+ ensure(ch, count * (fixedKey + fixedValue))
60
146
  end
61
147
 
62
- for k, v in value do
148
+ for i = 1, count do
149
+ local k = keys[i]
63
150
  keyCodec.write(ch, k)
64
- valueCodec.write(ch, v)
151
+ valueCodec.write(ch, value[k])
65
152
  end
153
+ releaseKeys()
66
154
  end,
67
155
 
68
156
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
69
- local count, lenBytes = Varint.read(src, pos)
70
- if maxCount and count > maxCount then
71
- error(`[Lync] Map.read: count {count} exceeds max {maxCount}`)
157
+ local count, lenBytes = varintRead(src, pos)
158
+ enforceMaxCount(count, maxCount)
159
+
160
+ -- Fixed-size: reject corrupt counts before per-entry decode.
161
+ if fixedKey and fixedValue and count > 0 then
162
+ local payloadBytes = count * (fixedKey + fixedValue)
163
+ if payloadBytes > bufLen(src) - (pos + lenBytes) then
164
+ Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
165
+ end
72
166
  end
73
167
 
74
168
  local result: { [any]: any } = {}
75
169
  local total = lenBytes
76
170
  for i = 1, count do
77
171
  local k, kc = keyCodec.read(src, pos + total, refs)
172
+ if kc == 0 then
173
+ Log.error(`truncated key at index {i}`)
174
+ end
78
175
  total += kc
79
176
  local v, vc = valueCodec.read(src, pos + total, refs)
80
- total += vc
81
- if kc == 0 and vc == 0 then
82
- error(`[Lync] Map.read: zero-byte entry at index {i}`)
177
+ if vc == 0 then
178
+ Log.error(`truncated value at index {i}`)
83
179
  end
180
+ total += vc
84
181
  result[k] = v
85
182
  end
86
183
  return result, total
87
184
  end,
88
- })
185
+ }
186
+ if hasUnknown then
187
+ codec._hasUnknown = true
188
+ end
189
+ if hasDelta then
190
+ codec._hasDelta = true
191
+ end
192
+ return table.freeze(codec)
89
193
  end
90
194
 
195
+ --[[
196
+ Per-key deltaMap. Wire:
197
+ [0] UNCHANGED (1 byte)
198
+ [1] FULL <count varint> <(k,v) pairs ...>
199
+ [2] DIFF <removeCount varint> <removed key bytes ...>
200
+ <changeCount varint> <(k,v) pair bytes ...>
201
+
202
+ Cache: per-key encoded value bytes for byte-equality diffing plus per-key
203
+ encoded key bytes so removed-key emission doesn't re-encode the key codec
204
+ for keys that no longer live in the map.
205
+ ]]
91
206
  function Map.deltaMap(
92
207
  keyCodec: Types.InternalCodec<any>,
93
208
  valueCodec: Types.InternalCodec<any>,
94
209
  maxCount: number?
95
210
  ): Types.InternalCodec<any>
211
+ -- Inner delta would chain-diff across pairs within a frame, not across frames.
212
+ if containsDelta(keyCodec) then
213
+ Log.error("deltaMap key codec cannot contain delta state")
214
+ end
215
+ if containsDelta(valueCodec) then
216
+ Log.error("deltaMap value codec cannot contain delta state")
217
+ end
218
+
96
219
  local deltaId = allocDeltaId()
97
- local innerMap = Map.map(keyCodec, valueCodec, maxCount)
220
+ local typeCheck = keyCodec._typeCheck
221
+ local canSortDirect = typeCheck == "number" or typeCheck == "string"
222
+ local inner = Map.map(keyCodec, valueCodec, maxCount)
223
+
224
+ --[[
225
+ Encode each (key, value) pair into `scratch` and capture per-pair byte
226
+ ranges. Returns the sorted-keys list plus four parallel arrays of
227
+ offset/length so the diff path can compare and emit individual entries
228
+ without re-encoding.
229
+ ]]
230
+ local function encodePerKey(
231
+ scratch: Types.ChannelState,
232
+ value: { [any]: any }
233
+ ): ({ any }, { number }, { number }, { number }, { number })
234
+ local keys, count = collectAndSortKeys(value, canSortDirect)
235
+ local kOff = table.create(count)
236
+ local kLen = table.create(count)
237
+ local vOff = table.create(count)
238
+ local vLen = table.create(count)
239
+
240
+ for i = 1, count do
241
+ local k = keys[i]
242
+ local before = scratch.cursor
243
+ keyCodec.write(scratch, k)
244
+ local mid = scratch.cursor
245
+ kOff[i] = before
246
+ kLen[i] = mid - before
247
+ valueCodec.write(scratch, value[k])
248
+ vOff[i] = mid
249
+ vLen[i] = scratch.cursor - mid
250
+ end
251
+ return keys, kOff, kLen, vOff, vLen
252
+ end
98
253
 
99
254
  return table.freeze({
100
255
  _isDelta = true,
256
+ _isMap = true,
257
+ _keyCodec = keyCodec,
258
+ _valueCodec = valueCodec,
259
+ _maxCount = maxCount,
101
260
 
102
261
  write = function(ch: Types.ChannelState, value: { [any]: any }): ()
103
- local cache = ch.deltas[deltaId]
262
+ if typeof(value) ~= "table" then
263
+ Log.error(`expected table, got {typeof(value)}`)
264
+ end
104
265
 
105
- if not cache then
106
- writeByte(ch, DELTA_FULL)
266
+ local cache = ch.deltas[deltaId] :: any
267
+ local scratch = acquireScratch()
268
+ local keys, kOff, kLen, vOff, vLen = encodePerKey(scratch, value)
269
+ local count = #keys
270
+ local scratchBuf = scratch.buff
107
271
 
108
- local scratch = acquireScratch()
109
- innerMap.write(scratch, value)
110
- local snapLen = scratch.cursor
272
+ -- First frame: emit FULL. Per-key scratch bytes ARE the inner map payload.
273
+ if not cache then
274
+ ensure(ch, 1)
275
+ writeu8(ch.buff, ch.cursor, DM_FULL)
276
+ ch.cursor += 1
277
+ varintWrite(ch, count)
278
+ local payloadLen = scratch.cursor
279
+ ensure(ch, payloadLen)
280
+ bufCopy(ch.buff, ch.cursor, scratchBuf, 0, payloadLen)
281
+ ch.cursor += payloadLen
111
282
 
112
- ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
113
- innerMap.write(ch, value)
283
+ local perKey: { [any]: DeltaMapEntry } = {}
284
+ for i = 1, count do
285
+ local kbLen = kLen[i]
286
+ local vbLen = vLen[i]
287
+ perKey[keys[i]] = {
288
+ kBuf = copyToBuf(nil, scratchBuf, kOff[i], kbLen),
289
+ kLen = kbLen,
290
+ vBuf = copyToBuf(nil, scratchBuf, vOff[i], vbLen),
291
+ vLen = vbLen,
292
+ }
293
+ end
294
+ ch.deltas[deltaId] = { perKey = perKey } :: any
295
+ releaseKeys()
114
296
  releaseScratch()
115
297
  return
116
298
  end
117
299
 
118
- local scratch = acquireScratch()
119
- innerMap.write(scratch, value)
120
- local scratchLen = scratch.cursor
121
-
122
- if
123
- scratchLen == cache.len
124
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
125
- then
126
- writeByte(ch, DELTA_UNCHANGED)
300
+ local cachedPerKey = cache.perKey :: { [any]: DeltaMapEntry }
301
+
302
+ local seen: { [any]: boolean } = {}
303
+ local changed: { number } = {}
304
+ local changedCount = 0
305
+ for i = 1, count do
306
+ local k = keys[i]
307
+ seen[k] = true
308
+ local entry = cachedPerKey[k]
309
+ local newLen = vLen[i]
310
+ if
311
+ not entry
312
+ or newLen ~= entry.vLen
313
+ or not rangeEqual(scratchBuf, vOff[i], entry.vBuf, 0, newLen)
314
+ then
315
+ changedCount += 1
316
+ changed[changedCount] = i
317
+ end
318
+ end
319
+
320
+ local removed: { any } = {}
321
+ local removedCount = 0
322
+ for k in cachedPerKey do
323
+ if not seen[k] then
324
+ removedCount += 1
325
+ removed[removedCount] = k
326
+ end
327
+ end
328
+
329
+ if removedCount == 0 and changedCount == 0 then
330
+ ensure(ch, 1)
331
+ writeu8(ch.buff, ch.cursor, DM_UNCHANGED)
332
+ ch.cursor += 1
333
+ releaseKeys()
127
334
  releaseScratch()
128
335
  return
129
336
  end
130
337
 
131
- writeByte(ch, DELTA_FULL)
132
- innerMap.write(ch, value)
338
+ -- Sort removed keys so the XOR baseline can collapse identical drops.
339
+ sortKeys(removed, canSortDirect)
340
+
341
+ ensure(ch, 1)
342
+ writeu8(ch.buff, ch.cursor, DM_DIFF)
343
+ ch.cursor += 1
344
+
345
+ varintWrite(ch, removedCount)
346
+ for i = 1, removedCount do
347
+ local entry = cachedPerKey[removed[i]]
348
+ local n = entry.kLen
349
+ ensure(ch, n)
350
+ bufCopy(ch.buff, ch.cursor, entry.kBuf, 0, n)
351
+ ch.cursor += n
352
+ end
353
+
354
+ varintWrite(ch, changedCount)
355
+ for i = 1, changedCount do
356
+ local idx = changed[i]
357
+ local kbLen = kLen[idx]
358
+ local vbLen = vLen[idx]
359
+ ensure(ch, kbLen + vbLen)
360
+ bufCopy(ch.buff, ch.cursor, scratchBuf, kOff[idx], kbLen)
361
+ bufCopy(ch.buff, ch.cursor + kbLen, scratchBuf, vOff[idx], vbLen)
362
+ ch.cursor += kbLen + vbLen
363
+ end
364
+
365
+ -- Update cache: drop removed, refresh changed, leave untouched in place.
366
+ for i = 1, removedCount do
367
+ cachedPerKey[removed[i]] = nil
368
+ end
369
+ for i = 1, changedCount do
370
+ local idx = changed[i]
371
+ local k = keys[idx]
372
+ local kbLen = kLen[idx]
373
+ local vbLen = vLen[idx]
374
+ local entry = cachedPerKey[k]
375
+ if entry then
376
+ entry.kBuf = copyToBuf(entry.kBuf, scratchBuf, kOff[idx], kbLen)
377
+ entry.kLen = kbLen
378
+ entry.vBuf = copyToBuf(entry.vBuf, scratchBuf, vOff[idx], vbLen)
379
+ entry.vLen = vbLen
380
+ else
381
+ cachedPerKey[k] = {
382
+ kBuf = copyToBuf(nil, scratchBuf, kOff[idx], kbLen),
383
+ kLen = kbLen,
384
+ vBuf = copyToBuf(nil, scratchBuf, vOff[idx], vbLen),
385
+ vLen = vbLen,
386
+ }
387
+ end
388
+ end
133
389
 
134
- cache.raw = snapshot(scratch.buff, scratchLen)
135
- cache.len = scratchLen
390
+ releaseKeys()
136
391
  releaseScratch()
137
392
  end,
138
393
 
139
394
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
140
- if readu8(src, pos) == DELTA_UNCHANGED then
141
- return {}, 1
395
+ if pos >= bufLen(src) then
396
+ Log.error("truncated deltaMap header")
397
+ end
398
+ local flag = readu8(src, pos)
399
+
400
+ if flag == DM_UNCHANGED then
401
+ local cached = Baseline.getCache(deltaId)
402
+ if cached == nil then
403
+ Log.error("UNCHANGED flag before any FULL frame; baseline missing")
404
+ end
405
+ return cached, 1
406
+ end
407
+
408
+ if flag == DM_FULL then
409
+ local value, consumed = inner.read(src, pos + 1, refs)
410
+ Baseline.setCache(deltaId, value)
411
+ return value, 1 + consumed
142
412
  end
143
- local value, consumed = innerMap.read(src, pos + 1, refs)
144
- return value, 1 + consumed
413
+
414
+ if flag == DM_DIFF then
415
+ local cached = Baseline.getCache(deltaId)
416
+ if cached == nil then
417
+ Log.error("deltaMap DIFF before any FULL frame; baseline missing")
418
+ end
419
+ local result: { [any]: any } = table.clone(cached)
420
+ local total = 1
421
+
422
+ local removeCount, rcLen = varintRead(src, pos + total)
423
+ if rcLen == 0 then
424
+ Log.error("truncated deltaMap remove count")
425
+ end
426
+ -- Both counts bounded by maxCount; otherwise a malicious DIFF
427
+ -- could spin the loop arbitrarily long before erroring on
428
+ -- buffer overrun.
429
+ enforceMaxCount(removeCount, maxCount, "removeCount")
430
+ total += rcLen
431
+ for _ = 1, removeCount do
432
+ local k, kc = keyCodec.read(src, pos + total, refs)
433
+ if kc == 0 then
434
+ Log.error("truncated deltaMap removed key")
435
+ end
436
+ total += kc
437
+ result[k] = nil
438
+ end
439
+
440
+ local changeCount, ccLen = varintRead(src, pos + total)
441
+ if ccLen == 0 then
442
+ Log.error("truncated deltaMap change count")
443
+ end
444
+ enforceMaxCount(changeCount, maxCount, "changeCount")
445
+ total += ccLen
446
+ for _ = 1, changeCount do
447
+ local k, kc = keyCodec.read(src, pos + total, refs)
448
+ if kc == 0 then
449
+ Log.error("truncated deltaMap changed key")
450
+ end
451
+ total += kc
452
+ local v, vc = valueCodec.read(src, pos + total, refs)
453
+ if vc == 0 then
454
+ Log.error("truncated deltaMap changed value")
455
+ end
456
+ total += vc
457
+ result[k] = v
458
+ end
459
+
460
+ Baseline.setCache(deltaId, result)
461
+ return result, total
462
+ end
463
+
464
+ Log.error(`invalid deltaMap flag {flag}`)
145
465
  end,
146
- })
466
+ }) :: Types.InternalCodec<any>
147
467
  end
148
468
 
149
469
  return table.freeze(Map)
@@ -1,14 +1,17 @@
1
1
  --!strict
2
- --!optimize 2
3
- -- Optional codec. 1-byte present/absent flag, value only if present.
2
+ --!native
3
+ -- Optional codec. 1-byte present/absent flag, value only when present.
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 ----------------------------------------------------------------
9
11
 
10
12
  local writeByte = Channel.writeByte
11
13
  local readu8 = buffer.readu8
14
+ local bufferLen = buffer.len
12
15
 
13
16
  -- Public -----------------------------------------------------------------
14
17
 
@@ -29,6 +32,7 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
29
32
  end,
30
33
 
31
34
  read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
35
+ Log.assert(pos < bufferLen(src), "presence byte missing at end of buffer")
32
36
  if readu8(src, pos) == 0 then
33
37
  return nil, 1
34
38
  end
@@ -37,9 +41,13 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
37
41
  end,
38
42
  }
39
43
 
44
+ -- Propagate _hasUnknown so Gate skips schema validation upstream.
40
45
  if inner._hasUnknown then
41
46
  codec._hasUnknown = true
42
47
  end
48
+ if Shared.containsDelta(inner) then
49
+ codec._hasDelta = true
50
+ end
43
51
  return table.freeze(codec)
44
52
  end
45
53