@axpecter/lync 2.3.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.
@@ -9,6 +9,13 @@ local Shared = require(script.Parent.Parent.composite.Shared)
9
9
  local Types = require(script.Parent.Parent.Parent.Types)
10
10
  local Varint = require(script.Parent.Parent.primitive.Varint)
11
11
 
12
+ -- Constants --------------------------------------------------------------
13
+
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
+
12
19
  -- State ------------------------------------------------------------------
13
20
 
14
21
  --[[
@@ -21,8 +28,22 @@ local _keyDepth = 0
21
28
 
22
29
  -- Private ----------------------------------------------------------------
23
30
 
24
- local alloc = Base.alloc
31
+ local ensure = Base.ensure
25
32
  local allocDeltaId = Shared.allocDeltaId
33
+ local acquireScratch = Shared.acquireScratch
34
+ local releaseScratch = Shared.releaseScratch
35
+ local rangeEqual = Shared.rangeEqual
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 }
26
47
 
27
48
  local function acquireKeys(): { any }
28
49
  _keyDepth += 1
@@ -44,6 +65,34 @@ local function tostringLess(a: any, b: any): boolean
44
65
  return tostring(a) < tostring(b)
45
66
  end
46
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
95
+
47
96
  -- Public -----------------------------------------------------------------
48
97
 
49
98
  local Map = {}
@@ -56,6 +105,7 @@ function Map.map(
56
105
  local fixedKey = keyCodec._size
57
106
  local fixedValue = valueCodec._size
58
107
  local hasUnknown = keyCodec._hasUnknown or valueCodec._hasUnknown
108
+ local hasDelta = containsDelta(keyCodec) or containsDelta(valueCodec)
59
109
 
60
110
  --[[
61
111
  Numbers and strings sort by `<` directly; everything else (Instance,
@@ -82,61 +132,35 @@ function Map.map(
82
132
  Log.error(`expected table, got {typeof(value)}`)
83
133
  end
84
134
 
85
- local keys = acquireKeys()
86
- local count = 0
87
- for k in value do
88
- count += 1
89
- keys[count] = k
90
- end
91
-
92
- -- Drop tail entries from the prior reuse so table.sort sees only `count` items.
93
- local prevTail = #keys
94
- for i = count + 1, prevTail do
95
- keys[i] = nil
96
- end
135
+ local keys, count = collectAndSortKeys(value, canSortDirect)
97
136
 
98
- Varint.write(ch, count)
137
+ varintWrite(ch, count)
99
138
  if count == 0 then
100
139
  releaseKeys()
101
140
  return
102
141
  end
103
142
 
104
- if canSortDirect then
105
- table.sort(keys :: { any })
106
- else
107
- table.sort(keys, tostringLess)
108
- end
109
-
110
143
  -- Single alloc when both halves are fixed-size.
111
144
  if fixedKey and fixedValue then
112
- local entrySize = fixedKey + fixedValue
113
- local cursor = ch.cursor
114
- if cursor + count * entrySize > ch.size then
115
- alloc(ch, count * entrySize)
116
- end
145
+ ensure(ch, count * (fixedKey + fixedValue))
117
146
  end
118
147
 
119
148
  for i = 1, count do
120
149
  local k = keys[i]
121
150
  keyCodec.write(ch, k)
122
151
  valueCodec.write(ch, value[k])
123
- keys[i] = nil
124
152
  end
125
153
  releaseKeys()
126
154
  end,
127
155
 
128
156
  read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
129
- local count, lenBytes = Varint.read(src, pos)
130
- if maxCount and count > maxCount then
131
- Log.error(`count {count} exceeds max {maxCount}`)
132
- end
157
+ local count, lenBytes = varintRead(src, pos)
158
+ enforceMaxCount(count, maxCount)
133
159
 
134
160
  -- Fixed-size: reject corrupt counts before per-entry decode.
135
161
  if fixedKey and fixedValue and count > 0 then
136
- local entrySize = fixedKey + fixedValue
137
- local dataStart = pos + lenBytes
138
- local payloadBytes = count * entrySize
139
- if payloadBytes > buffer.len(src) - dataStart then
162
+ local payloadBytes = count * (fixedKey + fixedValue)
163
+ if payloadBytes > bufLen(src) - (pos + lenBytes) then
140
164
  Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
141
165
  end
142
166
  end
@@ -162,19 +186,284 @@ function Map.map(
162
186
  if hasUnknown then
163
187
  codec._hasUnknown = true
164
188
  end
189
+ if hasDelta then
190
+ codec._hasDelta = true
191
+ end
165
192
  return table.freeze(codec)
166
193
  end
167
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
+ ]]
168
206
  function Map.deltaMap(
169
207
  keyCodec: Types.InternalCodec<any>,
170
208
  valueCodec: Types.InternalCodec<any>,
171
209
  maxCount: number?
172
210
  ): Types.InternalCodec<any>
173
- return Shared.makeUnchangedOrFullDelta(
174
- Map.map(keyCodec, valueCodec, maxCount),
175
- allocDeltaId(),
176
- Baseline
177
- )
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
+
219
+ local deltaId = allocDeltaId()
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
253
+
254
+ return table.freeze({
255
+ _isDelta = true,
256
+ _isMap = true,
257
+ _keyCodec = keyCodec,
258
+ _valueCodec = valueCodec,
259
+ _maxCount = maxCount,
260
+
261
+ write = function(ch: Types.ChannelState, value: { [any]: any }): ()
262
+ if typeof(value) ~= "table" then
263
+ Log.error(`expected table, got {typeof(value)}`)
264
+ end
265
+
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
271
+
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
282
+
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()
296
+ releaseScratch()
297
+ return
298
+ end
299
+
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()
334
+ releaseScratch()
335
+ return
336
+ end
337
+
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
389
+
390
+ releaseKeys()
391
+ releaseScratch()
392
+ end,
393
+
394
+ read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
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
412
+ end
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}`)
465
+ end,
466
+ }) :: Types.InternalCodec<any>
178
467
  end
179
468
 
180
469
  return table.freeze(Map)
@@ -4,6 +4,7 @@
4
4
 
5
5
  local Channel = require(script.Parent.Parent.Parent.internal.Channel)
6
6
  local Log = require(script.Parent.Parent.Parent.util.Log)
7
+ local Shared = require(script.Parent.Parent.composite.Shared)
7
8
  local Types = require(script.Parent.Parent.Parent.Types)
8
9
 
9
10
  -- Private ----------------------------------------------------------------
@@ -44,6 +45,9 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
44
45
  if inner._hasUnknown then
45
46
  codec._hasUnknown = true
46
47
  end
48
+ if Shared.containsDelta(inner) then
49
+ codec._hasDelta = true
50
+ end
47
51
  return table.freeze(codec)
48
52
  end
49
53
 
@@ -11,14 +11,14 @@ local Types = require(script.Parent.Parent.Parent.Types)
11
11
  -- Constants --------------------------------------------------------------
12
12
 
13
13
  --[[
14
- Delta flag space, shared across deltaStruct/deltaArray/deltaMap:
15
- 0 UNCHANGED (decoder reuses cache)
16
- 1 FULL (full payload follows)
17
- Struct's per-segment scheme uses [1..segCount] as a single-dirty-segment
18
- index, then segCount + DELTA_FULL_OFFSET / + DELTA_MULTI_OFFSET.
14
+ Delta flag base shared by deltaStruct's per-segment scheme:
15
+ 0 UNCHANGED
16
+ 1..segCount single dirty segment k
17
+ segCount + FULL_OFFSET FULL payload
18
+ segCount + MULTI_OFFSET MULTI: bitmap + dirty segs in order
19
+ deltaArray and deltaMap manage their own flag spaces.
19
20
  ]]
20
21
  local DELTA_UNCHANGED = 0
21
- local DELTA_FULL = 1
22
22
  local DELTA_FULL_OFFSET = 1
23
23
  local DELTA_MULTI_OFFSET = 2
24
24
 
@@ -37,9 +37,29 @@ local lshift = bit32.lshift
37
37
  local readu8 = buffer.readu8
38
38
  local writeu8 = buffer.writeu8
39
39
  local bufCopy = buffer.copy
40
+ local bufLen = buffer.len
40
41
  local rangeEqual = Buf.rangeEqual
41
42
  local snapshot = Buf.snapshot
42
43
 
44
+ --[[
45
+ Reuse `existing` when it's already large enough, else snapshot via
46
+ Buf.snapshot. Avoids per-write buffer allocations on delta caches that
47
+ hover at a stable size.
48
+ ]]
49
+ local function copyToBuf(existing: buffer?, src: buffer, srcOff: number, len: number): buffer
50
+ if existing and bufLen(existing) >= len then
51
+ bufCopy(existing, 0, src, srcOff, len)
52
+ return existing
53
+ end
54
+ return snapshot(src, len, srcOff)
55
+ end
56
+
57
+ -- Update a `{raw, len}` cache shape from scratch bytes; reuses raw via copyToBuf.
58
+ local function refreshCache(cache: any, src: buffer, len: number): ()
59
+ cache.raw = copyToBuf(cache.raw, src, 0, len)
60
+ cache.len = len
61
+ end
62
+
43
63
  -- Public -----------------------------------------------------------------
44
64
 
45
65
  local Shared = {}
@@ -49,6 +69,20 @@ Shared.DELTA_FULL_OFFSET = DELTA_FULL_OFFSET
49
69
  Shared.DELTA_MULTI_OFFSET = DELTA_MULTI_OFFSET
50
70
  Shared.rangeEqual = rangeEqual
51
71
  Shared.snapshot = snapshot
72
+ Shared.refreshCache = refreshCache
73
+ Shared.copyToBuf = copyToBuf
74
+
75
+ -- True when `c` itself is a delta codec OR contains one in its tree.
76
+ function Shared.containsDelta(c: Types.InternalCodec<any>): boolean
77
+ return c._isDelta == true or c._hasDelta == true
78
+ end
79
+
80
+ -- Common bounds check for variable-length container codecs.
81
+ function Shared.enforceMaxCount(len: number, maxCount: number?, label: string?): ()
82
+ if maxCount and len > maxCount then
83
+ Log.error(`{label or "count"} {len} exceeds max {maxCount}`)
84
+ end
85
+ end
52
86
 
53
87
  function Shared.allocDeltaId(): number
54
88
  _nextDeltaId += 1
@@ -82,6 +116,26 @@ function Shared.releaseScratch(): ()
82
116
  _scratchDepth -= 1
83
117
  end
84
118
 
119
+ --[[
120
+ Reset module-private state. Called by Sandbox in tests so that a thrown
121
+ codec.write (which skips releaseScratch and leaves depth inflated) can't
122
+ leak into the next test's scratch acquisition. Also resets the deltaId
123
+ counter so test ordering doesn't bleed.
124
+ ]]
125
+ function Shared.reset(): ()
126
+ _scratchDepth = 0
127
+ _nextDeltaId = 0
128
+ for i = 1, #_scratchStack do
129
+ local ch = _scratchStack[i]
130
+ ch.cursor = 0
131
+ ch.refCount = 0
132
+ if ch.refCount > 0 then
133
+ table.clear(ch.refs)
134
+ end
135
+ table.clear(ch.deltas)
136
+ end
137
+ end
138
+
85
139
  --[[
86
140
  Split a struct schema into (dataKeys, dataCodecs, boolKeys), each sorted
87
141
  alphabetically for deterministic wire layout.
@@ -234,79 +288,4 @@ function Shared.unpackBoolArray(
234
288
  return byteCount
235
289
  end
236
290
 
237
- --[[
238
- Build a byte-equality delta codec around any inner codec. Wire:
239
- [DELTA_UNCHANGED] 1 byte
240
- [DELTA_FULL] <inner-encoded payload> 1 + N bytes
241
- Used by deltaArray and deltaMap. The FULL path encodes once into scratch
242
- then bufCopies into the channel — avoids a double encode.
243
- ]]
244
- function Shared.makeUnchangedOrFullDelta(
245
- inner: Types.InternalCodec<any>,
246
- deltaId: number,
247
- Baseline: {
248
- getCache: (number) -> any,
249
- setCache: (number, any) -> (),
250
- }
251
- ): Types.InternalCodec<any>
252
- return table.freeze({
253
- _isDelta = true,
254
-
255
- write = function(ch: Types.ChannelState, value: any): ()
256
- local scratch = Shared.acquireScratch()
257
- inner.write(scratch, value)
258
- local scratchLen = scratch.cursor
259
-
260
- local cache = ch.deltas[deltaId]
261
- if
262
- cache
263
- and scratchLen == cache.len
264
- and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
265
- then
266
- local cursor = ch.cursor
267
- if cursor + 1 > ch.size then
268
- alloc(ch, 1)
269
- end
270
- writeu8(ch.buff, cursor, DELTA_UNCHANGED)
271
- ch.cursor = cursor + 1
272
- Shared.releaseScratch()
273
- return
274
- end
275
-
276
- local cursor = ch.cursor
277
- if cursor + 1 + scratchLen > ch.size then
278
- alloc(ch, 1 + scratchLen)
279
- end
280
- local b = ch.buff
281
- writeu8(b, cursor, DELTA_FULL)
282
- if scratchLen > 0 then
283
- bufCopy(b, cursor + 1, scratch.buff, 0, scratchLen)
284
- end
285
- ch.cursor = cursor + 1 + scratchLen
286
-
287
- local snap = snapshot(scratch.buff, scratchLen)
288
- if cache then
289
- cache.raw = snap
290
- cache.len = scratchLen
291
- else
292
- ch.deltas[deltaId] = { raw = snap, len = scratchLen }
293
- end
294
- Shared.releaseScratch()
295
- end,
296
-
297
- read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
298
- if readu8(src, pos) == DELTA_UNCHANGED then
299
- local cached = Baseline.getCache(deltaId)
300
- if not cached then
301
- Log.error("UNCHANGED flag before any FULL frame; baseline missing")
302
- end
303
- return cached, 1
304
- end
305
- local value, consumed = inner.read(src, pos + 1, refs)
306
- Baseline.setCache(deltaId, value)
307
- return value, 1 + consumed
308
- end,
309
- })
310
- end
311
-
312
291
  return table.freeze(Shared)
@@ -55,6 +55,7 @@ local packBools = Shared.packBools
55
55
  local unpackBools = Shared.unpackBools
56
56
  local rangeEqual = Shared.rangeEqual
57
57
  local snapshot = Shared.snapshot
58
+ local refreshCache = Shared.refreshCache
58
59
  local acquireScratch = Shared.acquireScratch
59
60
  local releaseScratch = Shared.releaseScratch
60
61
  local allocDeltaId = Shared.allocDeltaId
@@ -89,14 +90,18 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
89
90
  local boolCount = #boolKeys
90
91
  local boolBytes = Shared.boolBytes(boolCount)
91
92
 
92
- -- Single pass: detect all-direct fast-write path + propagate _hasUnknown.
93
+ -- Single pass: detect all-direct fast-write path + propagate _hasUnknown / _hasDelta.
93
94
  local allDirect, fixedSize = true, 0
94
95
  local hasUnknown = false
96
+ local hasDelta = false
95
97
  for i = 1, dataCount do
96
98
  local c = dataCodecs[i]
97
99
  if c._hasUnknown then
98
100
  hasUnknown = true
99
101
  end
102
+ if Shared.containsDelta(c) then
103
+ hasDelta = true
104
+ end
100
105
  if allDirect then
101
106
  if c._size and c._directWrite and c._directRead then
102
107
  fixedSize += c._size
@@ -194,6 +199,9 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
194
199
  if hasUnknown then
195
200
  codec._hasUnknown = true
196
201
  end
202
+ if hasDelta then
203
+ codec._hasDelta = true
204
+ end
197
205
  return table.freeze(codec)
198
206
  end
199
207
 
@@ -226,6 +234,9 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
226
234
  if hasUnknown then
227
235
  codec._hasUnknown = true
228
236
  end
237
+ if hasDelta then
238
+ codec._hasDelta = true
239
+ end
229
240
  return table.freeze(codec)
230
241
  end
231
242
 
@@ -421,8 +432,7 @@ function Struct.deltaStruct(
421
432
  end
422
433
  end
423
434
 
424
- cache.raw = snapshot(scratch.buff, scratchLen)
425
- cache.len = scratchLen;
435
+ refreshCache(cache, scratch.buff, scratchLen);
426
436
  (cache :: any).segOff = segOff;
427
437
  (cache :: any).segLen = segLen
428
438
  releaseScratch()