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